Support subscription invites (#5024)

This commit is contained in:
zubiden 2024-10-20 18:53:06 +02:00 committed by Alexander Zinchuk
parent 24a129c3e1
commit 28cecbfe3c
72 changed files with 1622 additions and 487 deletions

View File

@ -8,6 +8,7 @@ import type {
ApiChatBannedRights, ApiChatBannedRights,
ApiChatFolder, ApiChatFolder,
ApiChatInviteImporter, ApiChatInviteImporter,
ApiChatInviteInfo,
ApiChatlistExportedInvite, ApiChatlistExportedInvite,
ApiChatlistInvite, ApiChatlistInvite,
ApiChatMember, ApiChatMember,
@ -18,13 +19,14 @@ import type {
ApiRestrictionReason, ApiRestrictionReason,
ApiSendAsPeerId, ApiSendAsPeerId,
ApiSponsoredMessageReportResult, ApiSponsoredMessageReportResult,
ApiStarsSubscriptionPricing,
ApiTopic, ApiTopic,
} from '../../types'; } from '../../types';
import { omitUndefined, pick, pickTruthy } from '../../../util/iteratees'; import { omitUndefined, pick, pickTruthy } from '../../../util/iteratees';
import { getServerTime, getServerTimeOffset } from '../../../util/serverTime'; import { getServerTime, getServerTimeOffset } from '../../../util/serverTime';
import { serializeBytes } from '../helpers'; import { addPhotoToLocalDb, addUserToLocalDb, serializeBytes } from '../helpers';
import { buildApiUsernames, buildAvatarPhotoId } from './common'; import { buildApiPhoto, buildApiUsernames, buildAvatarPhotoId } from './common';
import { omitVirtualClassFields } from './helpers'; import { omitVirtualClassFields } from './helpers';
import { import {
buildApiEmojiStatus, buildApiEmojiStatus,
@ -67,6 +69,7 @@ function buildApiChatFieldsFromPeerEntity(
? buildApiEmojiStatus(peerEntity.emojiStatus) : undefined; ? buildApiEmojiStatus(peerEntity.emojiStatus) : undefined;
const boostLevel = ('level' in peerEntity) ? peerEntity.level : undefined; const boostLevel = ('level' in peerEntity) ? peerEntity.level : undefined;
const areProfilesShown = Boolean('signatureProfiles' in peerEntity && peerEntity.signatureProfiles); const areProfilesShown = Boolean('signatureProfiles' in peerEntity && peerEntity.signatureProfiles);
const subscriptionUntil = 'subscriptionUntilDate' in peerEntity ? peerEntity.subscriptionUntilDate : undefined;
return omitUndefined<PeerEntityApiChatFields>({ return omitUndefined<PeerEntityApiChatFields>({
isMin, isMin,
@ -100,6 +103,7 @@ function buildApiChatFieldsFromPeerEntity(
hasStories: Boolean(maxStoryId) && !storiesUnavailable, hasStories: Boolean(maxStoryId) && !storiesUnavailable,
emojiStatus, emojiStatus,
boostLevel, boostLevel,
subscriptionUntil,
}); });
} }
@ -670,3 +674,47 @@ export function buildApiSponsoredMessageReportResult(
options, options,
}; };
} }
export function buildApiChatInviteInfo(invite: GramJs.ChatInvite): ApiChatInviteInfo {
const {
color, participants, participantsCount, photo, title, about, scam, fake, verified, megagroup, channel, broadcast,
requestNeeded, subscriptionFormId, subscriptionPricing, canRefulfillSubscription,
} = invite;
let apiPhoto;
if (photo instanceof GramJs.Photo) {
addPhotoToLocalDb(photo);
apiPhoto = buildApiPhoto(photo);
}
participants?.forEach(addUserToLocalDb);
return {
title,
about,
isFake: fake,
isScam: scam,
isVerified: verified,
isSuperGroup: megagroup,
isPublic: invite.public,
participantsCount,
color,
isChannel: channel,
isBroadcast: broadcast,
isRequestNeeded: requestNeeded,
photo: apiPhoto,
subscriptionFormId: subscriptionFormId?.toString(),
subscriptionPricing: subscriptionPricing && buildApiStarsSubscriptionPricing(subscriptionPricing),
canRefulfillSubscription,
participantIds: participants?.map((participant) => buildApiPeerId(participant.id, 'user')).filter(Boolean),
};
}
export function buildApiStarsSubscriptionPricing(
pricing: GramJs.StarsSubscriptionPricing,
): ApiStarsSubscriptionPricing {
return {
period: pricing.period,
amount: pricing.amount.toJSNumber(),
};
}

View File

@ -20,6 +20,7 @@ import type {
ApiReceipt, ApiReceipt,
ApiStarGiveawayOption, ApiStarGiveawayOption,
ApiStarsGiveawayWinnerOption, ApiStarsGiveawayWinnerOption,
ApiStarsSubscription,
ApiStarsTransaction, ApiStarsTransaction,
ApiStarsTransactionPeer, ApiStarsTransactionPeer,
ApiStarTopupOption, ApiStarTopupOption,
@ -27,6 +28,7 @@ import type {
} from '../../types'; } from '../../types';
import { addWebDocumentToLocalDb } from '../helpers'; import { addWebDocumentToLocalDb } from '../helpers';
import { buildApiStarsSubscriptionPricing } from './chats';
import { buildApiMessageEntity } from './common'; import { buildApiMessageEntity } from './common';
import { omitVirtualClassFields } from './helpers'; import { omitVirtualClassFields } from './helpers';
import { buildApiDocument, buildApiWebDocument, buildMessageMediaContent } from './messageContent'; import { buildApiDocument, buildApiWebDocument, buildMessageMediaContent } from './messageContent';
@ -504,6 +506,7 @@ export function buildApiStarsTransactionPeer(peer: GramJs.TypeStarsTransactionPe
export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): ApiStarsTransaction { export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): ApiStarsTransaction {
const { const {
date, id, peer, stars, description, photo, title, refund, extendedMedia, failed, msgId, pending, gift, reaction, date, id, peer, stars, description, photo, title, refund, extendedMedia, failed, msgId, pending, gift, reaction,
subscriptionPeriod,
} = transaction; } = transaction;
if (photo) { if (photo) {
@ -527,10 +530,28 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction):
messageId: msgId, messageId: msgId,
isGift: gift, isGift: gift,
extendedMedia: boughtExtendedMedia, extendedMedia: boughtExtendedMedia,
subscriptionPeriod,
isReaction: reaction, isReaction: reaction,
}; };
} }
export function buildApiStarsSubscription(subscription: GramJs.StarsSubscription): ApiStarsSubscription {
const {
id, peer, pricing, untilDate, canRefulfill, canceled, chatInviteHash, missingBalance,
} = subscription;
return {
id,
peerId: getApiChatIdFromMtpPeer(peer),
until: untilDate,
pricing: buildApiStarsSubscriptionPricing(pricing),
isCancelled: canceled,
canRefulfill,
hasMissingBalance: missingBalance,
chatInviteHash,
};
}
export function buildApiStarTopupOption(option: GramJs.TypeStarsTopupOption): ApiStarTopupOption { export function buildApiStarTopupOption(option: GramJs.TypeStarsTopupOption): ApiStarTopupOption {
const { const {
amount, currency, stars, extended, amount, currency, stars, extended,

View File

@ -647,6 +647,12 @@ export function buildInputInvoice(invoice: ApiRequestInputInvoice) {
}); });
} }
case 'chatInviteSubscription': {
return new GramJs.InputInvoiceChatInviteSubscription({
hash: invoice.hash,
});
}
case 'giveaway': case 'giveaway':
default: { default: {
const purpose = buildInputStorePaymentPurpose(invoice.purpose); const purpose = buildInputStorePaymentPurpose(invoice.purpose);

View File

@ -38,6 +38,7 @@ import {
buildApiChatFromDialog, buildApiChatFromDialog,
buildApiChatFromPreview, buildApiChatFromPreview,
buildApiChatFromSavedDialog, buildApiChatFromSavedDialog,
buildApiChatInviteInfo,
buildApiChatlistExportedInvite, buildApiChatlistExportedInvite,
buildApiChatlistInvite, buildApiChatlistInvite,
buildApiChatReactions, buildApiChatReactions,
@ -1404,53 +1405,27 @@ export async function migrateChat(chat: ApiChat) {
return buildApiChatFromPreview(newChannel); return buildApiChatFromPreview(newChannel);
} }
export async function openChatByInvite(hash: string) { export async function checkChatInvite(hash: string) {
const result = await invokeRequest(new GramJs.messages.CheckChatInvite({ hash })); const result = await invokeRequest(new GramJs.messages.CheckChatInvite({ hash }));
if (!result) { if (!result) {
return undefined; return undefined;
} }
let chat: ApiChat | undefined;
if (result instanceof GramJs.ChatInvite) { if (result instanceof GramJs.ChatInvite) {
const { return {
photo, participantsCount, title, channel, requestNeeded, about, megagroup, chat: undefined,
} = result; invite: buildApiChatInviteInfo(result),
users: result.participants?.map(buildApiUser).filter(Boolean),
if (photo instanceof GramJs.Photo) { };
addPhotoToLocalDb(result.photo);
}
sendApiUpdate({
'@type': 'showInvite',
data: {
title,
about,
hash,
participantsCount,
isChannel: channel && !megagroup,
isRequestNeeded: requestNeeded,
...(photo instanceof GramJs.Photo && { photo: buildApiPhoto(photo) }),
},
});
} else {
chat = buildApiChatFromPreview(result.chat);
if (chat) {
sendApiUpdate({
'@type': 'updateChat',
id: chat.id,
chat,
});
}
} }
const chat = buildApiChatFromPreview(result.chat);
if (!chat) { if (!chat) {
return undefined; return undefined;
} }
return { chatId: chat.id }; return { chat, invite: undefined, users: undefined };
} }
export async function addChatMembers(chat: ApiChat, users: ApiUser[]) { export async function addChatMembers(chat: ApiChat, users: ApiUser[]) {

View File

@ -20,10 +20,12 @@ import {
buildApiReceipt, buildApiReceipt,
buildApiStarsGiftOptions, buildApiStarsGiftOptions,
buildApiStarsGiveawayOptions, buildApiStarsGiveawayOptions,
buildApiStarsSubscription,
buildApiStarsTransaction, buildApiStarsTransaction,
buildApiStarTopupOption, buildApiStarTopupOption,
buildShippingOptions, buildShippingOptions,
} from '../apiBuilders/payments'; } from '../apiBuilders/payments';
import { buildApiPeerId } from '../apiBuilders/peers';
import { import {
buildInputInvoice, buildInputPeer, buildInputStorePaymentPurpose, buildInputThemeParams, buildShippingInfo, buildInputInvoice, buildInputPeer, buildInputStorePaymentPurpose, buildInputThemeParams, buildShippingInfo,
} from '../gramjsBuilders'; } from '../gramjsBuilders';
@ -134,7 +136,7 @@ export async function sendStarPaymentForm({
invoice: buildInputInvoice(inputInvoice), invoice: buildInputInvoice(inputInvoice),
})); }));
if (!result) return false; if (!result) return undefined;
if (result instanceof GramJs.payments.PaymentVerificationNeeded) { if (result instanceof GramJs.payments.PaymentVerificationNeeded) {
if (DEBUG) { if (DEBUG) {
@ -143,11 +145,29 @@ export async function sendStarPaymentForm({
} }
return undefined; return undefined;
} else {
handleGramJsUpdate(result.updates);
} }
return Boolean(result); handleGramJsUpdate(result.updates);
if (inputInvoice.type === 'chatInviteSubscription') {
const updates = 'updates' in result.updates ? result.updates.updates : undefined;
const mtpChannelId = updates?.find((update): update is GramJs.UpdateChannel => (
update instanceof GramJs.UpdateChannel
))?.channelId;
if (!mtpChannelId) {
return undefined;
}
return {
channelId: buildApiPeerId(mtpChannelId, 'channel'),
};
}
return {
completed: true,
};
} }
export async function getPaymentForm(inputInvoice: ApiRequestInputInvoice, theme?: ApiThemeParameters) { export async function getPaymentForm(inputInvoice: ApiRequestInputInvoice, theme?: ApiThemeParameters) {
@ -416,8 +436,10 @@ export async function fetchStarsStatus() {
} }
return { return {
nextOffset: result.nextOffset, nextHistoryOffset: result.nextOffset,
history: result.history?.map(buildApiStarsTransaction), history: result.history?.map(buildApiStarsTransaction),
nextSubscriptionOffset: result.subscriptionsNextOffset,
subscriptions: result.subscriptions?.map(buildApiStarsSubscription),
balance: result.balance.toJSNumber(), balance: result.balance.toJSNumber(),
}; };
} }
@ -475,6 +497,59 @@ export async function fetchStarsTransactionById({
}; };
} }
export async function fetchStarsSubscriptions({
offset, peer,
}: {
offset?: string;
peer?: ApiPeer;
}) {
const inputPeer = peer ? buildInputPeer(peer.id, peer.accessHash) : new GramJs.InputPeerSelf();
const result = await invokeRequest(new GramJs.payments.GetStarsSubscriptions({
peer: inputPeer,
offset,
}));
if (!result?.subscriptions) {
return undefined;
}
return {
nextOffset: result.subscriptionsNextOffset,
subscriptions: result.subscriptions.map(buildApiStarsSubscription),
balance: result.balance.toJSNumber(),
};
}
export async function changeStarsSubscription({
peer, subscriptionId, isCancelled,
}: {
peer?: ApiPeer;
subscriptionId: string;
isCancelled: boolean;
}) {
const result = await invokeRequest(new GramJs.payments.ChangeStarsSubscription({
peer: peer ? buildInputPeer(peer.id, peer.accessHash) : new GramJs.InputPeerSelf(),
subscriptionId,
canceled: isCancelled,
}));
return result;
}
export async function fulfillStarsSubscription({
peer, subscriptionId,
}: {
peer?: ApiPeer;
subscriptionId: string;
}) {
const result = await invokeRequest(new GramJs.payments.FulfillStarsSubscription({
peer: peer ? buildInputPeer(peer.id, peer.accessHash) : new GramJs.InputPeerSelf(),
subscriptionId,
}));
return result;
}
export async function fetchStarsTopupOptions() { export async function fetchStarsTopupOptions() {
const result = await invokeRequest(new GramJs.payments.GetStarsTopupOptions()); const result = await invokeRequest(new GramJs.payments.GetStarsTopupOptions());

View File

@ -86,6 +86,8 @@ export interface ApiChat {
hasUnreadStories?: boolean; hasUnreadStories?: boolean;
maxStoryId?: number; maxStoryId?: number;
subscriptionUntil?: number;
// Locally determined field // Locally determined field
detectedLanguage?: string; detectedLanguage?: string;
} }

View File

@ -2,6 +2,7 @@ import type { ThreadId } from '../../types';
import type { ApiWebDocument } from './bots'; import type { ApiWebDocument } from './bots';
import type { ApiGroupCall, PhoneCallAction } from './calls'; import type { ApiGroupCall, PhoneCallAction } from './calls';
import type { ApiChat, ApiPeerColor } from './chats'; import type { ApiChat, ApiPeerColor } from './chats';
import type { ApiChatInviteInfo } from './misc';
import type { import type {
ApiInputStorePaymentPurpose, ApiInputStorePaymentPurpose,
ApiPremiumGiftCodeOption, ApiPremiumGiftCodeOption,
@ -263,8 +264,15 @@ export type ApiInputInvoiceStarsGiveaway = {
users: number; users: number;
}; };
export type ApiInputInvoiceChatInviteSubscription = {
type: 'chatInviteSubscription';
hash: string;
inviteInfo: ApiChatInviteInfo;
};
export type ApiInputInvoice = ApiInputInvoiceMessage | ApiInputInvoiceSlug | ApiInputInvoiceGiveaway export type ApiInputInvoice = ApiInputInvoiceMessage | ApiInputInvoiceSlug | ApiInputInvoiceGiveaway
| ApiInputInvoiceGiftCode | ApiInputInvoiceStarsGift | ApiInputInvoiceStars | ApiInputInvoiceStarsGiveaway; | ApiInputInvoiceGiftCode | ApiInputInvoiceStarsGift | ApiInputInvoiceStars | ApiInputInvoiceStarsGiveaway
| ApiInputInvoiceChatInviteSubscription;
/* Used for Invoice request */ /* Used for Invoice request */
export type ApiRequestInputInvoiceMessage = { export type ApiRequestInputInvoiceMessage = {
@ -294,8 +302,14 @@ export type ApiRequestInputInvoiceStarsGiveaway = {
purpose: ApiInputStorePaymentPurpose; purpose: ApiInputStorePaymentPurpose;
}; };
export type ApiRequestInputInvoiceChatInviteSubscription = {
type: 'chatInviteSubscription';
hash: string;
};
export type ApiRequestInputInvoice = ApiRequestInputInvoiceMessage | ApiRequestInputInvoiceSlug export type ApiRequestInputInvoice = ApiRequestInputInvoiceMessage | ApiRequestInputInvoiceSlug
| ApiRequestInputInvoiceGiveaway | ApiRequestInputInvoiceStars | ApiRequestInputInvoiceStarsGiveaway; | ApiRequestInputInvoiceGiveaway | ApiRequestInputInvoiceStars | ApiRequestInputInvoiceStarsGiveaway
| ApiRequestInputInvoiceChatInviteSubscription;
export interface ApiInvoice { export interface ApiInvoice {
mediaType: 'invoice'; mediaType: 'invoice';

View File

@ -134,16 +134,6 @@ export type ApiFieldError = {
message: string; message: string;
}; };
export type ApiInviteInfo = {
title: string;
about?: string;
hash: string;
isChannel?: boolean;
participantsCount?: number;
isRequestNeeded?: true;
photo?: ApiPhoto;
};
export type ApiExportedInvite = { export type ApiExportedInvite = {
isRevoked?: boolean; isRevoked?: boolean;
isPermanent?: boolean; isPermanent?: boolean;
@ -159,6 +149,31 @@ export type ApiExportedInvite = {
adminId: string; adminId: string;
}; };
export type ApiChatInviteInfo = {
title: string;
about?: string;
photo?: ApiPhoto;
isScam?: boolean;
isFake?: boolean;
isChannel?: boolean;
isVerified?: boolean;
isSuperGroup?: boolean;
isPublic?: boolean;
participantsCount?: number;
participantIds?: string[];
color: number;
isBroadcast?: boolean;
isRequestNeeded?: boolean;
subscriptionFormId?: string;
canRefulfillSubscription?: boolean;
subscriptionPricing?: ApiStarsSubscriptionPricing;
};
export type ApiStarsSubscriptionPricing = {
period: number;
amount: number;
};
export type ApiChatInviteImporter = { export type ApiChatInviteImporter = {
userId: string; userId: string;
date: number; date: number;

View File

@ -5,6 +5,7 @@ import type { ApiChat } from './chats';
import type { import type {
ApiDocument, ApiMessageEntity, ApiPaymentCredentials, BoughtPaidMedia, ApiDocument, ApiMessageEntity, ApiPaymentCredentials, BoughtPaidMedia,
} from './messages'; } from './messages';
import type { ApiStarsSubscriptionPricing } from './misc';
import type { StatisticsOverviewPercentage } from './statistics'; import type { StatisticsOverviewPercentage } from './statistics';
import type { ApiUser } from './users'; import type { ApiUser } from './users';
@ -311,6 +312,18 @@ export interface ApiStarsTransaction {
description?: string; description?: string;
photo?: ApiWebDocument; photo?: ApiWebDocument;
extendedMedia?: BoughtPaidMedia[]; extendedMedia?: BoughtPaidMedia[];
subscriptionPeriod?: number;
}
export interface ApiStarsSubscription {
id: string;
peerId: string;
until: number;
pricing: ApiStarsSubscriptionPricing;
isCancelled?: true;
canRefulfill?: true;
hasMissingBalance?: true;
chatInviteHash?: string;
} }
export interface ApiStarTopupOption { export interface ApiStarTopupOption {

View File

@ -33,7 +33,7 @@ import type {
BoughtPaidMedia, BoughtPaidMedia,
} from './messages'; } from './messages';
import type { import type {
ApiEmojiInteraction, ApiError, ApiInviteInfo, ApiNotifyException, ApiSessionData, ApiEmojiInteraction, ApiError, ApiNotifyException, ApiSessionData,
} from './misc'; } from './misc';
import type { ApiStealthMode, ApiStory, ApiStorySkipped } from './stories'; import type { ApiStealthMode, ApiStory, ApiStorySkipped } from './stories';
import type { import type {
@ -114,11 +114,6 @@ export type ApiUpdateChatJoin = {
id: string; id: string;
}; };
export type ApiUpdateShowInvite = {
'@type': 'showInvite';
data: ApiInviteInfo;
};
export type ApiUpdateChatLeave = { export type ApiUpdateChatLeave = {
'@type': 'updateChatLeave'; '@type': 'updateChatLeave';
id: string; id: string;
@ -788,7 +783,7 @@ export type ApiUpdate = (
ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateMessageTranslations | ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateMessageTranslations |
ApiUpdateTwoFaError | ApiUpdatePasswordError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent | ApiUpdateTwoFaError | ApiUpdatePasswordError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent |
ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy | ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy |
ApiUpdateServerTimeOffset | ApiUpdateShowInvite | ApiUpdateMessageReactions | ApiUpdateSavedReactionTags | ApiUpdateServerTimeOffset | ApiUpdateMessageReactions | ApiUpdateSavedReactionTags |
ApiUpdateGroupCallParticipants | ApiUpdateGroupCallConnection | ApiUpdateGroupCall | ApiUpdateGroupCallStreams | ApiUpdateGroupCallParticipants | ApiUpdateGroupCallConnection | ApiUpdateGroupCall | ApiUpdateGroupCallStreams |
ApiUpdateGroupCallConnectionState | ApiUpdateGroupCallLeavePresentation | ApiUpdateGroupCallChatId | ApiUpdateGroupCallConnectionState | ApiUpdateGroupCallLeavePresentation | ApiUpdateGroupCallChatId |
ApiUpdatePendingJoinRequests | ApiUpdatePaymentVerificationNeeded | ApiUpdatePaymentStateCompleted | ApiUpdatePendingJoinRequests | ApiUpdatePaymentVerificationNeeded | ApiUpdatePaymentStateCompleted |

View File

@ -1281,7 +1281,9 @@
"AriaSearchOlderResult" = "Focus next result"; "AriaSearchOlderResult" = "Focus next result";
"AriaSearchNewerResult" = "Focus previous result"; "AriaSearchNewerResult" = "Focus previous result";
"CreditsBoxHistoryEntryGiftOutAbout" = "With Stars, {user} will be able to unlock content and services on Telegram. {link}" "CreditsBoxHistoryEntryGiftOutAbout" = "With Stars, {user} will be able to unlock content and services on Telegram. {link}"
"CreditsBoxOutAbout" = "Review the {link} for Stars." "StarsTransactionTOS" = "Review the {link} for Stars."
"StarsTransactionTOSLinkText" = "Terms of Service"
"StarsTransactionTOSLink" = "https://telegram.org/tos/stars"
"GiftStarsOutgoing" = "With Stars, {user} will be able to unlock content and services on Telegram." "GiftStarsOutgoing" = "With Stars, {user} will be able to unlock content and services on Telegram."
"SendPaidReaction" = "Send ⭐️{amount}" "SendPaidReaction" = "Send ⭐️{amount}"
"StarsReactionTerms" = "By sending Stars you agree to the {link}" "StarsReactionTerms" = "By sending Stars you agree to the {link}"
@ -1290,3 +1292,9 @@
"MiniAppsMoreTabs_one" = "{botName} & {count} Other"; "MiniAppsMoreTabs_one" = "{botName} & {count} Other";
"MiniAppsMoreTabs_other" = "{botName} & {count} Others"; "MiniAppsMoreTabs_other" = "{botName} & {count} Others";
"PrizeCredits" = "Your prize is {count} Stars." "PrizeCredits" = "Your prize is {count} Stars."
"StarsSubscribeText_one" = "Do you want to subscribe to **{chat}** for **{amount} Star** per month?"
"StarsSubscribeText_other" = "Do you want to subscribe to **{chat}** for **{amount} Stars** per month?"
"StarsSubscribeInfo" = "By subscribing you agree to the {link}"
"StarsSubscribeInfoLinkText" = "Terms of Service"
"StarsSubscribeInfoLink" = "https://telegram.org/tos/stars"
"StarsPerMonth" = "⭐️{amount}/month"

View File

@ -18,19 +18,14 @@ export { default as AttachBotInstallModal } from '../components/modals/attachBot
export { default as DeleteFolderDialog } from '../components/main/DeleteFolderDialog'; export { default as DeleteFolderDialog } from '../components/main/DeleteFolderDialog';
export { default as PremiumMainModal } from '../components/main/premium/PremiumMainModal'; export { default as PremiumMainModal } from '../components/main/premium/PremiumMainModal';
export { default as PremiumGiftModal } from '../components/main/premium/PremiumGiftModal'; export { default as PremiumGiftModal } from '../components/main/premium/PremiumGiftModal';
export { default as StarsGiftModal } from '../components/main/premium/StarsGiftModal';
export { default as GiveawayModal } from '../components/main/premium/GiveawayModal'; export { default as GiveawayModal } from '../components/main/premium/GiveawayModal';
export { default as PremiumGiftingPickerModal } from '../components/main/premium/PremiumGiftingPickerModal'; export { default as PremiumGiftingPickerModal } from '../components/main/premium/PremiumGiftingPickerModal';
export { default as StarsGiftingPickerModal } from '../components/main/premium/StarsGiftingPickerModal';
export { default as PremiumLimitReachedModal } from '../components/main/premium/common/PremiumLimitReachedModal'; export { default as PremiumLimitReachedModal } from '../components/main/premium/common/PremiumLimitReachedModal';
export { default as StatusPickerMenu } from '../components/left/main/StatusPickerMenu'; export { default as StatusPickerMenu } from '../components/left/main/StatusPickerMenu';
export { default as BoostModal } from '../components/modals/boost/BoostModal'; export { default as BoostModal } from '../components/modals/boost/BoostModal';
export { default as GiftCodeModal } from '../components/modals/giftcode/GiftCodeModal'; export { default as GiftCodeModal } from '../components/modals/giftcode/GiftCodeModal';
export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal'; export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal';
export { default as StarsBalanceModal } from '../components/modals/stars/StarsBalanceModal'; export { default as ChatInviteModal } from '../components/modals/chatInvite/ChatInviteModal';
export { default as StarPaymentModal } from '../components/modals/stars/StarsPaymentModal';
export { default as StarsTransactionInfoModal } from '../components/modals/stars/transaction/StarsTransactionModal';
export { default as PaidReactionModal } from '../components/modals/paidReaction/PaidReactionModal';
export { default as AboutAdsModal } from '../components/common/AboutAdsModal'; export { default as AboutAdsModal } from '../components/common/AboutAdsModal';
export { default as AboutMonetizationModal } from '../components/common/AboutMonetizationModal'; export { default as AboutMonetizationModal } from '../components/common/AboutMonetizationModal';

7
src/bundles/stars.ts Normal file
View File

@ -0,0 +1,7 @@
export { default as StarsGiftModal } from '../components/main/premium/StarsGiftModal';
export { default as StarsGiftingPickerModal } from '../components/main/premium/StarsGiftingPickerModal';
export { default as StarsBalanceModal } from '../components/modals/stars/StarsBalanceModal';
export { default as StarPaymentModal } from '../components/modals/stars/StarsPaymentModal';
export { default as StarsTransactionInfoModal } from '../components/modals/stars/transaction/StarsTransactionModal';
export { default as StarsSubscriptionModal } from '../components/modals/stars/subscription/StarsSubscriptionModal';
export { default as PaidReactionModal } from '../components/modals/paidReaction/PaidReactionModal';

View File

@ -220,6 +220,9 @@ const Avatar: FC<OwnProps> = ({
} else if (chat) { } else if (chat) {
const title = getChatTitle(lang, chat); const title = getChatTitle(lang, chat);
content = title && getFirstLetters(title, isUserId(chat.id) ? 2 : 1); content = title && getFirstLetters(title, isUserId(chat.id) ? 2 : 1);
} else if (isCustomPeer) {
const title = peer.title || lang(peer.titleKey!);
content = title && getFirstLetters(title, 1);
} else if (text) { } else if (text) {
content = getFirstLetters(text, 2); content = getFirstLetters(text, 2);
} }

View File

@ -9,7 +9,7 @@
} }
.fullName { .fullName {
font-size: 1rem; font-size: 1em;
margin-bottom: 0; margin-bottom: 0;
&.canCopy { &.canCopy {

View File

@ -80,7 +80,7 @@ const FullNameTitle: FC<OwnProps> = ({
const specialTitle = useMemo(() => { const specialTitle = useMemo(() => {
if (customPeer) { if (customPeer) {
return lang(customPeer.titleKey, customPeer.titleValue, 'i'); return customPeer.title || lang(customPeer.titleKey!);
} }
if (isSavedMessages) { if (isSavedMessages) {
@ -125,8 +125,8 @@ const FullNameTitle: FC<OwnProps> = ({
</h3> </h3>
{!iconElement && peer && ( {!iconElement && peer && (
<> <>
{!noVerified && realPeer?.isVerified && <VerifiedIcon />} {!noVerified && peer?.isVerified && <VerifiedIcon />}
{!noFake && realPeer?.fakeType && <FakeIcon fakeType={realPeer.fakeType} />} {!noFake && peer?.fakeType && <FakeIcon fakeType={peer.fakeType} />}
{withEmojiStatus && realPeer?.emojiStatus && ( {withEmojiStatus && realPeer?.emojiStatus && (
<CustomEmoji <CustomEmoji
documentId={realPeer.emojiStatus.documentId} documentId={realPeer.emojiStatus.documentId}

View File

@ -14,7 +14,7 @@ import styles from './PeerBadge.module.scss';
type OwnProps = { type OwnProps = {
peer: ApiPeer | CustomPeer; peer: ApiPeer | CustomPeer;
text?: string; text?: string;
badgeText: string; badgeText?: string;
badgeIcon?: IconName; badgeIcon?: IconName;
className?: string; className?: string;
badgeClassName?: string; badgeClassName?: string;
@ -37,10 +37,12 @@ const PeerBadge = ({
> >
<div className={styles.top}> <div className={styles.top}>
<Avatar size="large" peer={peer} /> <Avatar size="large" peer={peer} />
<div className={buildClassName(styles.badge, badgeClassName)}> {badgeText && (
{badgeIcon && <Icon name={badgeIcon} />} <div className={buildClassName(styles.badge, badgeClassName)}>
{badgeText} {badgeIcon && <Icon name={badgeIcon} />}
</div> {badgeText}
</div>
)}
</div> </div>
{text && <p className={styles.text}>{text}</p>} {text && <p className={styles.text}>{text}</p>}
</div> </div>

View File

@ -10,7 +10,7 @@ export function getPeerColorClass(peer?: ApiPeer | CustomPeer, noUserColors?: bo
} }
if ('isCustomPeer' in peer) { if ('isCustomPeer' in peer) {
if (!peer.peerColorId) return undefined; if (peer.peerColorId === undefined) return undefined;
return `peer-color-${peer.peerColorId}`; return `peer-color-${peer.peerColorId}`;
} }
return noUserColors ? `peer-color-count-${getPeerColorCount(peer)}` : `peer-color-${getPeerColorKey(peer)}`; return noUserColors ? `peer-color-count-${getPeerColorCount(peer)}` : `peer-color-${getPeerColorKey(peer)}`;

View File

@ -80,7 +80,7 @@ const PickerSelectedItem = <T,>({
/> />
); );
const name = (customPeer && lang(customPeer.titleKey)) const name = (customPeer && (customPeer.title || lang(customPeer.titleKey!)))
|| (!chat || (user && !isSavedMessages) || (!chat || (user && !isSavedMessages)
? getUserFirstOrLastName(user) ? getUserFirstOrLastName(user)
: getChatTitle(lang, chat, isSavedMessages)); : getChatTitle(lang, chat, isSavedMessages));

View File

@ -1,3 +1,5 @@
@use "../../../styles/mixins";
.Chat { .Chat {
--background-color: var(--color-background); --background-color: var(--color-background);
--thumbs-background: var(--background-color); --thumbs-background: var(--background-color);
@ -39,8 +41,8 @@
&:hover, &:hover,
&.ListItem.has-menu-open { &.ListItem.has-menu-open {
.avatar-online { .avatar-badge {
border-color: var(--color-chat-hover); --_color-outline: var(--color-chat-hover);
} }
.avatar-badge-wrapper { .avatar-badge-wrapper {
@ -70,8 +72,8 @@
&.selected { &.selected {
--background-color: var(--color-chat-hover) !important; --background-color: var(--color-chat-hover) !important;
.avatar-online { .avatar-badge {
border-color: var(--color-chat-hover); --_color-outline: var(--color-chat-hover);
} }
.ChatCallStatus { .ChatCallStatus {
@ -94,8 +96,11 @@
--color-checkmark: var(--color-primary); --color-checkmark: var(--color-primary);
} }
.avatar-badge {
--_color-outline: var(--color-chat-active) !important;
}
.avatar-online { .avatar-online {
border-color: var(--color-chat-active) !important;
background-color: var(--color-white); background-color: var(--color-white);
} }
@ -241,16 +246,25 @@
} }
} }
.avatar-online { .avatar-badge {
--_color-outline: var(--color-background);
position: absolute; position: absolute;
bottom: 0.0625rem; bottom: 0.0625rem;
right: 0.0625rem; right: 0.0625rem;
flex-shrink: 0;
}
.avatar-subscription {
@include mixins.filter-outline(1px, var(--_color-outline));
}
.avatar-online {
border-radius: 50%;
border: 2px solid var(--_color-outline);
background-color: #0ac630;
width: 0.875rem; width: 0.875rem;
height: 0.875rem; height: 0.875rem;
border-radius: 50%;
border: 2px solid var(--color-background);
background-color: #0ac630;
flex-shrink: 0;
opacity: 0.5; opacity: 0.5;
transform: scale(0); transform: scale(0);

View File

@ -64,6 +64,7 @@ import useChatListEntry from './hooks/useChatListEntry';
import Avatar from '../../common/Avatar'; import Avatar from '../../common/Avatar';
import DeleteChatModal from '../../common/DeleteChatModal'; import DeleteChatModal from '../../common/DeleteChatModal';
import FullNameTitle from '../../common/FullNameTitle'; import FullNameTitle from '../../common/FullNameTitle';
import StarIcon from '../../common/icons/StarIcon';
import LastMessageMeta from '../../common/LastMessageMeta'; import LastMessageMeta from '../../common/LastMessageMeta';
import ReportModal from '../../common/ReportModal'; import ReportModal from '../../common/ReportModal';
import ListItem from '../../ui/ListItem'; import ListItem from '../../ui/ListItem';
@ -345,12 +346,17 @@ const Chat: FC<OwnProps & StateProps> = ({
isSavedDialog={isSavedDialog} isSavedDialog={isSavedDialog}
size={isPreview ? 'medium' : 'large'} size={isPreview ? 'medium' : 'large'}
withStory={!user?.isSelf} withStory={!user?.isSelf}
withStoryGap={isAvatarOnlineShown} withStoryGap={isAvatarOnlineShown || Boolean(chat.subscriptionUntil)}
storyViewerOrigin={StoryViewerOrigin.ChatList} storyViewerOrigin={StoryViewerOrigin.ChatList}
storyViewerMode="single-peer" storyViewerMode="single-peer"
/> />
<div className="avatar-badge-wrapper"> <div className="avatar-badge-wrapper">
<div className={buildClassName('avatar-online', isAvatarOnlineShown && 'avatar-online-shown')} /> <div
className={buildClassName('avatar-online', 'avatar-badge', isAvatarOnlineShown && 'avatar-online-shown')}
/>
{!isAvatarOnlineShown && Boolean(chat.subscriptionUntil) && (
<StarIcon type="gold" className="avatar-badge avatar-subscription" size="adaptive" />
)}
<ChatBadge <ChatBadge
chat={chat} chat={chat}
isMuted={isMuted} isMuted={isMuted}

View File

@ -3,7 +3,7 @@ import React, { memo, useEffect } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { import type {
ApiContact, ApiError, ApiInviteInfo, ApiPhoto, ApiContact, ApiError,
} from '../../api/types'; } from '../../api/types';
import type { MessageList } from '../../global/types'; import type { MessageList } from '../../global/types';
@ -15,21 +15,18 @@ import renderText from '../common/helpers/renderText';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import Avatar from '../common/Avatar';
import Button from '../ui/Button'; import Button from '../ui/Button';
import Modal from '../ui/Modal'; import Modal from '../ui/Modal';
type StateProps = { type StateProps = {
currentMessageList?: MessageList; currentMessageList?: MessageList;
dialogs: (ApiError | ApiInviteInfo | ApiContact)[]; dialogs: (ApiError | ApiContact)[];
}; };
const Dialogs: FC<StateProps> = ({ dialogs, currentMessageList }) => { const Dialogs: FC<StateProps> = ({ dialogs, currentMessageList }) => {
const { const {
dismissDialog, dismissDialog,
acceptInviteConfirmation,
sendMessage, sendMessage,
showNotification,
} = getActions(); } = getActions();
const [isModalOpen, openModal, closeModal] = useFlag(); const [isModalOpen, openModal, closeModal] = useFlag();
@ -45,77 +42,6 @@ const Dialogs: FC<StateProps> = ({ dialogs, currentMessageList }) => {
return undefined; return undefined;
} }
function renderInviteHeader(title: string, photo?: ApiPhoto) {
return (
<div className="modal-header">
{photo && <Avatar size="small" photo={photo} withVideo />}
<div className="modal-title">
{renderText(title)}
</div>
<Button round color="translucent" size="smaller" ariaLabel={lang('Close')} onClick={closeModal}>
<i className="icon icon-close" />
</Button>
</div>
);
}
const renderInvite = (invite: ApiInviteInfo) => {
const {
hash, title, about, participantsCount, isChannel, photo, isRequestNeeded,
} = invite;
const handleJoinClick = () => {
acceptInviteConfirmation({
hash,
});
if (isRequestNeeded) {
showNotification({
message: isChannel ? lang('RequestToJoinChannelSentDescription') : lang('RequestToJoinGroupSentDescription'),
});
}
closeModal();
};
const participantsText = isChannel
? lang('Subscribers', participantsCount, 'i')
: lang('Members', participantsCount, 'i');
const joinText = isChannel ? lang('ChannelJoin') : lang('JoinGroup');
const requestToJoinText = isChannel
? lang('MemberRequests.RequestToJoinChannel') : lang('MemberRequests.RequestToJoinGroup');
return (
<Modal
isOpen={isModalOpen}
onClose={closeModal}
className="error"
header={renderInviteHeader(title, photo)}
onCloseAnimationEnd={dismissDialog}
>
{participantsCount !== undefined && <p className="modal-help">{participantsText}</p>}
{about && <p className="modal-about">{renderText(about, ['br'])}</p>}
{isRequestNeeded && (
<p className="modal-help">
{isChannel
? lang('MemberRequests.RequestToJoinDescriptionChannel')
: lang('MemberRequests.RequestToJoinDescriptionGroup')}
</p>
)}
<div className="dialog-buttons mt-2">
<Button
isText
className="confirm-dialog-button"
// eslint-disable-next-line react/jsx-no-bind
onClick={handleJoinClick}
>
{isRequestNeeded ? requestToJoinText : joinText}
</Button>
<Button isText className="confirm-dialog-button" onClick={closeModal}>{lang('Cancel')}</Button>
</div>
</Modal>
);
};
const renderContactRequest = (contactRequest: ApiContact) => { const renderContactRequest = (contactRequest: ApiContact) => {
const handleConfirm = () => { const handleConfirm = () => {
if (!currentMessageList) { if (!currentMessageList) {
@ -171,11 +97,7 @@ const Dialogs: FC<StateProps> = ({ dialogs, currentMessageList }) => {
); );
}; };
const renderDialog = (dialog: ApiError | ApiInviteInfo | ApiContact) => { const renderDialog = (dialog: ApiError | ApiContact) => {
if ('hash' in dialog) {
return renderInvite(dialog);
}
if ('phoneNumber' in dialog) { if ('phoneNumber' in dialog) {
return renderContactRequest(dialog); return renderContactRequest(dialog);
} }

View File

@ -9,7 +9,7 @@ import useModuleLoader from '../../../hooks/useModuleLoader';
const StarsGiftModalAsync: FC<OwnProps> = (props) => { const StarsGiftModalAsync: FC<OwnProps> = (props) => {
const { isOpen } = props; const { isOpen } = props;
const StarsGiftModal = useModuleLoader(Bundles.Extra, 'StarsGiftModal', !isOpen); const StarsGiftModal = useModuleLoader(Bundles.Stars, 'StarsGiftModal', !isOpen);
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
return StarsGiftModal ? <StarsGiftModal {...props} /> : undefined; return StarsGiftModal ? <StarsGiftModal {...props} /> : undefined;

View File

@ -9,7 +9,7 @@ import useModuleLoader from '../../../hooks/useModuleLoader';
const StarsGiftingPickerModalAsync: FC<OwnProps> = (props) => { const StarsGiftingPickerModalAsync: FC<OwnProps> = (props) => {
const { isOpen } = props; const { isOpen } = props;
const StarsGiftingPickerModal = useModuleLoader(Bundles.Extra, 'StarsGiftingPickerModal', !isOpen); const StarsGiftingPickerModal = useModuleLoader(Bundles.Stars, 'StarsGiftingPickerModal', !isOpen);
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
return StarsGiftingPickerModal ? <StarsGiftingPickerModal {...props} /> : undefined; return StarsGiftingPickerModal ? <StarsGiftingPickerModal {...props} /> : undefined;

View File

@ -8,6 +8,7 @@ import { pick } from '../../util/iteratees';
import AttachBotInstallModal from './attachBotInstall/AttachBotInstallModal.async'; import AttachBotInstallModal from './attachBotInstall/AttachBotInstallModal.async';
import BoostModal from './boost/BoostModal.async'; import BoostModal from './boost/BoostModal.async';
import ChatInviteModal from './chatInvite/ChatInviteModal.async';
import ChatlistModal from './chatlist/ChatlistModal.async'; import ChatlistModal from './chatlist/ChatlistModal.async';
import CollectibleInfoModal from './collectible/CollectibleInfoModal.async'; import CollectibleInfoModal from './collectible/CollectibleInfoModal.async';
import GiftCodeModal from './giftcode/GiftCodeModal.async'; import GiftCodeModal from './giftcode/GiftCodeModal.async';
@ -18,6 +19,7 @@ import PaidReactionModal from './paidReaction/PaidReactionModal.async';
import ReportAdModal from './reportAd/ReportAdModal.async'; import ReportAdModal from './reportAd/ReportAdModal.async';
import StarsBalanceModal from './stars/StarsBalanceModal.async'; import StarsBalanceModal from './stars/StarsBalanceModal.async';
import StarsPaymentModal from './stars/StarsPaymentModal.async'; import StarsPaymentModal from './stars/StarsPaymentModal.async';
import StarsSubscriptionModal from './stars/subscription/StarsSubscriptionModal.async';
import StarsTransactionInfoModal from './stars/transaction/StarsTransactionModal.async'; import StarsTransactionInfoModal from './stars/transaction/StarsTransactionModal.async';
import UrlAuthModal from './urlAuth/UrlAuthModal.async'; import UrlAuthModal from './urlAuth/UrlAuthModal.async';
import WebAppModal from './webApp/WebAppModal.async'; import WebAppModal from './webApp/WebAppModal.async';
@ -39,7 +41,9 @@ type ModalKey = keyof Pick<TabState,
'starsTransactionModal' | 'starsTransactionModal' |
'paidReactionModal' | 'paidReactionModal' |
'webApps' | 'webApps' |
'starsTransactionModal' 'starsTransactionModal' |
'chatInviteModal' |
'starsSubscriptionModal'
>; >;
type StateProps = { type StateProps = {
@ -69,7 +73,9 @@ const MODALS: ModalRegistry = {
isStarPaymentModalOpen: StarsPaymentModal, isStarPaymentModalOpen: StarsPaymentModal,
starsBalanceModal: StarsBalanceModal, starsBalanceModal: StarsBalanceModal,
starsTransactionModal: StarsTransactionInfoModal, starsTransactionModal: StarsTransactionInfoModal,
chatInviteModal: ChatInviteModal,
paidReactionModal: PaidReactionModal, paidReactionModal: PaidReactionModal,
starsSubscriptionModal: StarsSubscriptionModal,
}; };
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[]; const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>; const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;

View File

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

View File

@ -0,0 +1,31 @@
.content {
display: flex;
flex-direction: column;
align-items: center;
}
.title {
font-size: 1.5rem;
}
.participantCount {
color: var(--color-text-secondary);
}
.participants {
display: flex;
overflow-x: scroll;
gap: 0.5rem;
align-self: stretch;
}
.participant {
min-width: 4.5rem;
width: 4.5rem;
margin-inline: auto;
}
.buttons {
align-self: flex-end;
margin-top: 0.5rem;
}

View File

@ -0,0 +1,108 @@
import React, { memo, useMemo, useRef } from '../../../lib/teact/teact';
import { getActions, getGlobal } from '../../../global';
import type { TabState } from '../../../global/types';
import { getCustomPeerFromInvite, getUserFullName } from '../../../global/helpers';
import { selectUser } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import usePrevious from '../../../hooks/usePrevious';
import Avatar from '../../common/Avatar';
import FullNameTitle from '../../common/FullNameTitle';
import PeerBadge from '../../common/PeerBadge';
import Button from '../../ui/Button';
import Modal from '../../ui/Modal';
import styles from './ChatInviteModal.module.scss';
export type OwnProps = {
modal: TabState['chatInviteModal'];
};
const ChatInviteModal = ({ modal }: OwnProps) => {
const { acceptChatInvite, closeChatInviteModal, showNotification } = getActions();
// eslint-disable-next-line no-null/no-null
const participantsRef = useRef<HTMLDivElement>(null);
const lang = useOldLang();
const prevModal = usePrevious(modal);
const { hash, inviteInfo } = modal || prevModal || {};
const {
about, isBroadcast, participantIds, participantsCount, photo, isRequestNeeded,
} = inviteInfo || {};
const handleClose = useLastCallback(() => {
closeChatInviteModal();
});
const handleAccept = useLastCallback(() => {
acceptChatInvite({ hash: hash! });
showNotification({
message: isBroadcast ? lang('RequestToJoinChannelSentDescription') : lang('RequestToJoinGroupSentDescription'),
});
handleClose();
});
const acceptLangKey = isBroadcast ? 'ProfileJoinChannel' : 'JoinGroup';
const requestToJoinLangKey = isBroadcast ? 'MemberRequests.RequestToJoinChannel'
: 'MemberRequests.RequestToJoinGroup';
const customPeer = useMemo(() => {
if (!inviteInfo) return undefined;
return getCustomPeerFromInvite(inviteInfo);
}, [inviteInfo]);
const participants = useMemo(() => {
if (!participantIds) {
return undefined;
}
const global = getGlobal();
return participantIds.map((id) => selectUser(global, id)).filter(Boolean);
}, [participantIds]);
useHorizontalScroll(participantsRef, !modal || !participants);
return (
<Modal
isOpen={Boolean(modal)}
contentClassName={styles.content}
isSlim
onClose={handleClose}
onEnter={handleAccept}
>
{customPeer && <Avatar size="jumbo" photo={photo} peer={customPeer} withVideo />}
{customPeer && <FullNameTitle className={styles.title} peer={customPeer} />}
{about && <p className={styles.about}>{about}</p>}
<p className={styles.participantCount}>
{lang(isBroadcast ? 'Subscribers' : 'Members', participantsCount, 'i')}
</p>
{participants && (
<div ref={participantsRef} className={buildClassName(styles.participants, 'no-scrollbar')}>
{participants.map((participant) => (
<PeerBadge className={styles.participant} peer={participant} text={getUserFullName(participant)} />
))}
</div>
)}
<div className={buildClassName('dialog-buttons', styles.buttons)}>
<Button isText className="confirm-dialog-button" onClick={handleAccept}>
{lang(isRequestNeeded ? requestToJoinLangKey : acceptLangKey)}
</Button>
<Button isText className="confirm-dialog-button" onClick={handleClose}>
{lang('Cancel')}
</Button>
</div>
</Modal>
);
};
export default memo(ChatInviteModal);

View File

@ -13,56 +13,28 @@
} }
.value { .value {
background-color: var(--color-background);
word-break: break-word; word-break: break-word;
min-width: 2rem;
} }
.table { .table {
border-collapse: separate; display: grid;
border-spacing: 0; grid-template-columns: max-content 1fr;
border-radius: 0.3125rem;
border: 1px solid var(--color-borders);
background-color: var(--color-borders);
gap: 1px;
overflow: hidden;
} }
.cell { .cell {
border: solid 0.0625rem var(--color-borders);
border-style: none solid solid none;
padding: 0.25rem 0.5rem;
}
.row:first-child .cell:first-child { border-top-left-radius: 0.3125rem; }
.row:first-child .cell:last-child { border-top-right-radius: 0.3125rem; }
.row:last-child .cell:first-child { border-bottom-left-radius: 0.3125rem; }
.row:last-child .cell:last-child { border-bottom-right-radius: 0.3125rem; }
.row:first-child .cell { border-top-style: solid; }
.row .cell:first-child { border-left-style: solid; }
.section {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
padding: 0.25rem 0.5rem;
padding: 0.5rem;
position: relative; position: relative;
min-height: 2.5rem;
@include mixins.adapt-padding-to-scrollbar(0.5rem);
}
.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;
} }
.avatar { .avatar {

View File

@ -1,7 +1,7 @@
import React, { memo, type TeactNode } from '../../../lib/teact/teact'; import React, { memo, type TeactNode } from '../../../lib/teact/teact';
import { getActions } from '../../../global'; import { getActions } from '../../../global';
import type { ApiPeer, ApiWebDocument } from '../../../api/types'; import type { ApiPeer } from '../../../api/types';
import type { CustomPeer } from '../../../types'; import type { CustomPeer } from '../../../types';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
@ -15,8 +15,6 @@ import Modal from '../../ui/Modal';
import styles from './TableInfoModal.module.scss'; import styles from './TableInfoModal.module.scss';
import StarsBackground from '../../../assets/stars-bg.png';
type ChatItem = { chatId: string }; type ChatItem = { chatId: string };
export type TableData = [TeactNode, TeactNode | ChatItem][]; export type TableData = [TeactNode, TeactNode | ChatItem][];
@ -25,13 +23,7 @@ type OwnProps = {
isOpen?: boolean; isOpen?: boolean;
title?: string; title?: string;
tableData?: TableData; tableData?: TableData;
headerImageUrl?: string;
logoBackground?: string;
headerAvatarPeer?: ApiPeer | CustomPeer; headerAvatarPeer?: ApiPeer | CustomPeer;
headerAvatarWebPhoto?: ApiWebDocument;
noHeaderImage?: boolean;
isGift?: boolean;
isPrizeStars?: boolean;
header?: TeactNode; header?: TeactNode;
footer?: TeactNode; footer?: TeactNode;
buttonText?: string; buttonText?: string;
@ -44,13 +36,7 @@ const TableInfoModal = ({
isOpen, isOpen,
title, title,
tableData, tableData,
headerImageUrl,
logoBackground,
headerAvatarPeer, headerAvatarPeer,
headerAvatarWebPhoto,
noHeaderImage,
isGift,
isPrizeStars,
header, header,
footer, footer,
buttonText, buttonText,
@ -64,8 +50,6 @@ const TableInfoModal = ({
onClose(); onClose();
}); });
const withAvatar = Boolean(headerAvatarPeer || headerAvatarWebPhoto);
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
@ -77,23 +61,15 @@ const TableInfoModal = ({
contentClassName={styles.content} contentClassName={styles.content}
onClose={onClose} onClose={onClose}
> >
{!isGift && !isPrizeStars && !noHeaderImage && ( {headerAvatarPeer && (
withAvatar ? ( <Avatar peer={headerAvatarPeer} size="jumbo" className={styles.avatar} />
<Avatar peer={headerAvatarPeer} webPhoto={headerAvatarWebPhoto} size="jumbo" className={styles.avatar} />
) : (
<div className={styles.section}>
<img className={styles.logo} src={headerImageUrl} alt="" draggable={false} />
{Boolean(logoBackground)
&& <img className={styles.logoBackground} src={StarsBackground} alt="" draggable={false} />}
</div>
)
)} )}
{header} {header}
<table className={styles.table}> <div className={styles.table}>
{tableData?.map(([label, value]) => ( {tableData?.map(([label, value]) => (
<tr className={styles.row}> <>
<td className={buildClassName(styles.cell, styles.title)}>{label}</td> <div className={buildClassName(styles.cell, styles.title)}>{label}</div>
<td className={buildClassName(styles.cell, styles.value)}> <div className={buildClassName(styles.cell, styles.value)}>
{typeof value === 'object' && 'chatId' in value ? ( {typeof value === 'object' && 'chatId' in value ? (
<PickerSelectedItem <PickerSelectedItem
peerId={value.chatId} peerId={value.chatId}
@ -104,10 +80,10 @@ const TableInfoModal = ({
onClick={handleOpenChat} onClick={handleOpenChat}
/> />
) : value} ) : value}
</td> </div>
</tr> </>
))} ))}
</table> </div>
{footer} {footer}
{buttonText && ( {buttonText && (
<Button size="smaller" onClick={onButtonClick || onClose}>{buttonText}</Button> <Button size="smaller" onClick={onButtonClick || onClose}>{buttonText}</Button>

View File

@ -6,3 +6,10 @@
.centered { .centered {
text-align: center !important; text-align: center !important;
} }
.logo {
width: 7.5rem;
height: 7.5rem;
margin-bottom: 1rem;
align-self: center;
}

View File

@ -69,6 +69,7 @@ const GiftCodeModal = ({
const header = ( const header = (
<> <>
<img src={PremiumLogo} alt="" className={styles.logo} />
<p className={styles.centered}>{renderText(lang('lng_gift_link_about'), ['simple_markdown'])}</p> <p className={styles.centered}>{renderText(lang('lng_gift_link_about'), ['simple_markdown'])}</p>
<LinkField title="BoostingGiftLink" link={`${TME_LINK_PREFIX}/${GIFTCODE_PATH}/${slug}`} /> <LinkField title="BoostingGiftLink" link={`${TME_LINK_PREFIX}/${GIFTCODE_PATH}/${slug}`} />
</> </>
@ -110,7 +111,6 @@ const GiftCodeModal = ({
<TableInfoModal <TableInfoModal
isOpen={isOpen} isOpen={isOpen}
title={lang('lng_gift_link_title')} title={lang('lng_gift_link_title')}
headerImageUrl={PremiumLogo}
tableData={modalData.tableData} tableData={modalData.tableData}
header={modalData.header} header={modalData.header}
footer={modalData.footer} footer={modalData.footer}

View File

@ -9,7 +9,7 @@ import useModuleLoader from '../../../hooks/useModuleLoader';
const PaidReactionModalAsync: FC<OwnProps> = (props) => { const PaidReactionModalAsync: FC<OwnProps> = (props) => {
const { modal } = props; const { modal } = props;
const PaidReactionModal = useModuleLoader(Bundles.Extra, 'PaidReactionModal', !modal); const PaidReactionModal = useModuleLoader(Bundles.Stars, 'PaidReactionModal', !modal);
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
return PaidReactionModal ? <PaidReactionModal {...props} /> : undefined; return PaidReactionModal ? <PaidReactionModal {...props} /> : undefined;

View File

@ -9,7 +9,7 @@ import useModuleLoader from '../../../hooks/useModuleLoader';
const StarsBalanceModalAsync: FC<OwnProps> = (props) => { const StarsBalanceModalAsync: FC<OwnProps> = (props) => {
const { modal } = props; const { modal } = props;
const StarsBalanceModal = useModuleLoader(Bundles.Extra, 'StarsBalanceModal', !modal); const StarsBalanceModal = useModuleLoader(Bundles.Stars, 'StarsBalanceModal', !modal);
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
return StarsBalanceModal ? <StarsBalanceModal {...props} /> : undefined; return StarsBalanceModal ? <StarsBalanceModal {...props} /> : undefined;

View File

@ -42,6 +42,14 @@
@include mixins.side-panel-section; @include mixins.side-panel-section;
} }
.sectionTitle {
color: var(--color-primary);
font-weight: 500;
font-size: 1rem;
align-self: flex-start;
padding: 0.25rem 0.75rem;
}
.secondaryInfo { .secondaryInfo {
font-size: 0.875rem; font-size: 0.875rem;
color: var(--color-text-secondary); color: var(--color-text-secondary);
@ -76,6 +84,7 @@
margin-inline: 0.5rem; margin-inline: 0.5rem;
margin-bottom: 1rem; margin-bottom: 1rem;
text-wrap: balance; text-wrap: balance;
line-height: 1.375;
} }
.header { .header {
@ -123,10 +132,11 @@
} }
.balanceBottom { .balanceBottom {
line-height: 1.5;
font-weight: 500; font-weight: 500;
display: flex; display: flex;
align-items: center;
gap: 0.25rem; gap: 0.25rem;
line-height: 1.5;
} }
.modalBalance { .modalBalance {
@ -162,6 +172,17 @@
z-index: 1; z-index: 1;
} }
.avatarStar {
font-size: 2rem;
@include mixins.filter-outline(1px, var(--color-background));
position: absolute;
right: -1rem;
bottom: 0;
z-index: 1;
}
.paymentImageBackground { .paymentImageBackground {
height: 7rem; height: 7rem;
position: absolute; position: absolute;
@ -179,13 +200,27 @@
.paymentButton { .paymentButton {
display: flex; display: flex;
gap: 0.125rem; gap: 0.125rem;
margin-top: 1rem;
} }
.paymentButtonStar { .paymentButtonStar {
--color-fill: white !important; --color-fill: white !important;
} }
.transactions { .transactions, .subscriptions {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%;
}
.tabs {
// Disable tabs rounded corners
--border-radius-messages-small: 0;
top: 3.5rem;
}
.disclaimer {
margin-top: 0.5rem;
color: var(--color-text-secondary);
} }

View File

@ -26,7 +26,8 @@ import TabList, { type TabWithProperties } from '../../ui/TabList';
import Transition from '../../ui/Transition'; import Transition from '../../ui/Transition';
import BalanceBlock from './BalanceBlock'; import BalanceBlock from './BalanceBlock';
import StarTopupOptionList from './StarTopupOptionList'; import StarTopupOptionList from './StarTopupOptionList';
import TransactionItem from './transaction/StarsTransactionItem'; import StarsSubscriptionItem from './subscription/StarsSubscriptionItem';
import StarsTransactionItem from './transaction/StarsTransactionItem';
import styles from './StarsBalanceModal.module.scss'; import styles from './StarsBalanceModal.module.scss';
@ -39,6 +40,7 @@ const TRANSACTION_TABS: TabWithProperties[] = [
{ title: 'StarsTransactionsIncoming' }, { title: 'StarsTransactionsIncoming' },
{ title: 'StarsTransactionsOutgoing' }, { title: 'StarsTransactionsOutgoing' },
]; ];
const TRANSACTION_ITEM_CLASS = 'StarsTransactionItem';
export type OwnProps = { export type OwnProps = {
modal: TabState['starsBalanceModal']; modal: TabState['starsBalanceModal'];
@ -56,7 +58,7 @@ const StarsBalanceModal = ({
closeStarsBalanceModal, loadStarsTransactions, openStarsGiftingModal, openInvoice, closeStarsBalanceModal, loadStarsTransactions, openStarsGiftingModal, openInvoice,
} = getActions(); } = getActions();
const { balance, history } = starsBalanceState || {}; const { balance, history, subscriptions } = starsBalanceState || {};
const oldLang = useOldLang(); const oldLang = useOldLang();
const lang = useLang(); const lang = useLang();
@ -90,13 +92,14 @@ const StarsBalanceModal = ({
return undefined; return undefined;
}, [oldLang, originPayment, originReaction, starsNeeded]); }, [oldLang, originPayment, originReaction, starsNeeded]);
const shouldShowTransactions = Boolean(history?.all?.transactions.length && !originPayment && !originReaction); const shouldShowItems = Boolean(history?.all?.transactions.length && !originPayment && !originReaction);
const shouldSuggestGifting = !originPayment && !originReaction; const shouldSuggestGifting = !originPayment && !originReaction;
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
setHeaderHidden(true); setHeaderHidden(true);
setSelectedTabIndex(0); setSelectedTabIndex(0);
hideBuyOptions();
} }
}, [isOpen]); }, [isOpen]);
@ -127,7 +130,7 @@ const StarsBalanceModal = ({
setHeaderHidden(scrollTop <= 150); setHeaderHidden(scrollTop <= 150);
} }
const handleLoadMore = useLastCallback(() => { const handleLoadMoreTransactions = useLastCallback(() => {
loadStarsTransactions({ loadStarsTransactions({
type: TRANSACTION_TYPES[selectedTabIndex], type: TRANSACTION_TYPES[selectedTabIndex],
}); });
@ -170,7 +173,7 @@ const StarsBalanceModal = ({
<img className={styles.logo} src={StarLogo} alt="" draggable={false} /> <img className={styles.logo} src={StarLogo} alt="" draggable={false} />
<img className={styles.logoBackground} src={StarsBackground} alt="" draggable={false} /> <img className={styles.logoBackground} src={StarsBackground} alt="" draggable={false} />
<h2 className={styles.headerText}> <h2 className={styles.headerText}>
{starsNeeded ? oldLang('StarsNeededTitle', starsNeeded) : oldLang('TelegramStars')} {starsNeeded ? oldLang('StarsNeededTitle', ongoingTransactionAmount) : oldLang('TelegramStars')}
</h2> </h2>
<div className={styles.description}> <div className={styles.description}>
{renderText( {renderText(
@ -207,7 +210,20 @@ const StarsBalanceModal = ({
<div className={styles.secondaryInfo}> <div className={styles.secondaryInfo}>
{tosText} {tosText}
</div> </div>
{shouldShowTransactions && ( {shouldShowItems && Boolean(subscriptions?.list.length) && (
<div className={styles.section}>
<h3 className={styles.sectionTitle}>{oldLang('StarMySubscriptions')}</h3>
<div className={styles.subscriptions}>
{subscriptions?.list.map((subscription) => (
<StarsSubscriptionItem
key={subscription.id}
subscription={subscription}
/>
))}
</div>
</div>
)}
{shouldShowItems && (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.section}> <div className={styles.section}>
<Transition <Transition
@ -218,21 +234,25 @@ const StarsBalanceModal = ({
className={styles.transition} className={styles.transition}
> >
<InfiniteScroll <InfiniteScroll
onLoadMore={handleLoadMore} onLoadMore={handleLoadMoreTransactions}
items={history?.[TRANSACTION_TYPES[selectedTabIndex]]?.transactions} items={history?.[TRANSACTION_TYPES[selectedTabIndex]]?.transactions}
scrollContainerClosest={`.${styles.main}`}
itemSelector={`.${TRANSACTION_ITEM_CLASS}`}
className={styles.transactions} className={styles.transactions}
noFastList noFastList
> >
{history?.[TRANSACTION_TYPES[selectedTabIndex]]?.transactions.map((transaction) => ( {history?.[TRANSACTION_TYPES[selectedTabIndex]]?.transactions.map((transaction) => (
<TransactionItem <StarsTransactionItem
key={`${transaction.id}-${transaction.isRefund}`} key={`${transaction.id}-${transaction.isRefund}`}
transaction={transaction} transaction={transaction}
className={TRANSACTION_ITEM_CLASS}
/> />
))} ))}
</InfiniteScroll> </InfiniteScroll>
</Transition> </Transition>
</div> </div>
<TabList <TabList
className={styles.tabs}
activeTab={selectedTabIndex} activeTab={selectedTabIndex}
tabs={TRANSACTION_TABS} tabs={TRANSACTION_TABS}
onSwitchTab={setSelectedTabIndex} onSwitchTab={setSelectedTabIndex}

View File

@ -9,7 +9,7 @@ import useModuleLoader from '../../../hooks/useModuleLoader';
const StarPaymentModalAsync: FC<OwnProps> = (props) => { const StarPaymentModalAsync: FC<OwnProps> = (props) => {
const { modal } = props; const { modal } = props;
const StarPaymentModal = useModuleLoader(Bundles.Extra, 'StarPaymentModal', !modal); const StarPaymentModal = useModuleLoader(Bundles.Stars, 'StarPaymentModal', !modal);
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
return StarPaymentModal ? <StarPaymentModal {...props} /> : undefined; return StarPaymentModal ? <StarPaymentModal {...props} /> : undefined;

View File

@ -2,11 +2,11 @@ import React, { memo, useEffect, useMemo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { import type {
ApiChat, ApiMediaExtendedPreview, ApiMessage, ApiUser, ApiChat, ApiChatInviteInfo, ApiMediaExtendedPreview, ApiMessage, ApiUser,
} from '../../../api/types'; } from '../../../api/types';
import type { GlobalState, TabState } from '../../../global/types'; import type { GlobalState, TabState } from '../../../global/types';
import { getChatTitle, getUserFullName } from '../../../global/helpers'; import { getChatTitle, getCustomPeerFromInvite, getUserFullName } from '../../../global/helpers';
import { import {
selectChat, selectChatMessage, selectTabState, selectUser, selectChat, selectChatMessage, selectTabState, selectUser,
} from '../../../global/selectors'; } from '../../../global/selectors';
@ -14,11 +14,13 @@ import buildClassName from '../../../util/buildClassName';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import Avatar from '../../common/Avatar'; import Avatar from '../../common/Avatar';
import StarIcon from '../../common/icons/StarIcon'; import StarIcon from '../../common/icons/StarIcon';
import SafeLink from '../../common/SafeLink';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import Modal from '../../ui/Modal'; import Modal from '../../ui/Modal';
import BalanceBlock from './BalanceBlock'; import BalanceBlock from './BalanceBlock';
@ -38,6 +40,7 @@ type StateProps = {
bot?: ApiUser; bot?: ApiUser;
paidMediaMessage?: ApiMessage; paidMediaMessage?: ApiMessage;
paidMediaChat?: ApiChat; paidMediaChat?: ApiChat;
inviteInfo?: ApiChatInviteInfo;
}; };
const StarPaymentModal = ({ const StarPaymentModal = ({
@ -47,6 +50,7 @@ const StarPaymentModal = ({
payment, payment,
paidMediaMessage, paidMediaMessage,
paidMediaChat, paidMediaChat,
inviteInfo,
}: OwnProps & StateProps) => { }: OwnProps & StateProps) => {
const { closePaymentModal, openStarsBalanceModal, sendStarPaymentForm } = getActions(); const { closePaymentModal, openStarsBalanceModal, sendStarPaymentForm } = getActions();
const [isLoading, markLoading, unmarkLoading] = useFlag(); const [isLoading, markLoading, unmarkLoading] = useFlag();
@ -54,7 +58,8 @@ const StarPaymentModal = ({
const photo = payment?.invoice?.photo; const photo = payment?.invoice?.photo;
const lang = useOldLang(); const oldLang = useOldLang();
const lang = useLang();
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
@ -68,23 +73,54 @@ const StarPaymentModal = ({
} }
const botName = getUserFullName(bot); const botName = getUserFullName(bot);
const starsText = lang('Stars.Intro.PurchasedText.Stars', payment.invoice.amount); const starsText = oldLang('Stars.Intro.PurchasedText.Stars', payment.invoice.amount);
if (paidMediaMessage) { if (paidMediaMessage) {
const extendedMedia = paidMediaMessage.content.paidMedia!.extendedMedia as ApiMediaExtendedPreview[]; const extendedMedia = paidMediaMessage.content.paidMedia!.extendedMedia as ApiMediaExtendedPreview[];
const areAllPhotos = extendedMedia.every((media) => !media.duration); const areAllPhotos = extendedMedia.every((media) => !media.duration);
const areAllVideos = extendedMedia.every((media) => !!media.duration); const areAllVideos = extendedMedia.every((media) => !!media.duration);
const mediaText = areAllPhotos ? lang('Stars.Transfer.Photos', extendedMedia.length) const mediaText = areAllPhotos ? oldLang('Stars.Transfer.Photos', extendedMedia.length)
: areAllVideos ? lang('Stars.Transfer.Videos', extendedMedia.length) : areAllVideos ? oldLang('Stars.Transfer.Videos', extendedMedia.length)
: lang('Media', extendedMedia.length); : oldLang('Media', extendedMedia.length);
const channelTitle = getChatTitle(lang, paidMediaChat!); const channelTitle = getChatTitle(oldLang, paidMediaChat!);
return lang('Stars.Transfer.UnlockInfo', [mediaText, channelTitle, starsText]); return oldLang('Stars.Transfer.UnlockInfo', [mediaText, channelTitle, starsText]);
} }
return lang('Stars.Transfer.Info', [payment.invoice.title, botName, starsText]); if (inviteInfo) {
}, [payment?.invoice, bot, lang, paidMediaMessage, paidMediaChat]); return lang('StarsSubscribeText', {
chat: inviteInfo.title,
amount: payment.invoice.amount,
}, {
withNodes: true,
withMarkdown: true,
pluralValue: payment.invoice.amount,
});
}
return oldLang('Stars.Transfer.Info', [payment.invoice.title, botName, starsText]);
}, [payment?.invoice, bot, oldLang, lang, paidMediaMessage, paidMediaChat, inviteInfo]);
const disclaimerText = useMemo(() => {
if (inviteInfo) {
return lang('StarsSubscribeInfo', {
link: <SafeLink url={lang('StarsSubscribeInfoLink')} text={lang('StarsSubscribeInfoLinkText')} />,
}, {
withNodes: true,
});
}
return undefined;
}, [inviteInfo, lang]);
const inviteCustomPeer = useMemo(() => {
if (!inviteInfo) {
return undefined;
}
return getCustomPeerFromInvite(inviteInfo);
}, [inviteInfo]);
const handlePayment = useLastCallback(() => { const handlePayment = useLastCallback(() => {
const price = payment?.invoice?.amount; const price = payment?.invoice?.amount;
@ -113,9 +149,14 @@ const StarPaymentModal = ({
onClose={closePaymentModal} onClose={closePaymentModal}
> >
<BalanceBlock balance={starsBalanceState?.balance || 0} className={styles.modalBalance} /> <BalanceBlock balance={starsBalanceState?.balance || 0} className={styles.modalBalance} />
<div className={styles.paymentImages} dir={lang.isRtl ? 'ltr' : 'rtl'}> <div className={styles.paymentImages} dir={oldLang.isRtl ? 'ltr' : 'rtl'}>
{paidMediaMessage ? ( {paidMediaMessage ? (
<PaidMediaThumb media={paidMediaMessage.content.paidMedia!.extendedMedia} /> <PaidMediaThumb media={paidMediaMessage.content.paidMedia!.extendedMedia} />
) : inviteCustomPeer ? (
<>
<Avatar className={styles.paymentPhoto} peer={inviteCustomPeer} size="giant" />
<StarIcon type="gold" size="adaptive" className={styles.avatarStar} />
</>
) : ( ) : (
<> <>
<Avatar peer={bot} size="giant" /> <Avatar peer={bot} size="giant" />
@ -125,18 +166,23 @@ const StarPaymentModal = ({
<img className={styles.paymentImageBackground} src={StarsBackground} alt="" draggable={false} /> <img className={styles.paymentImageBackground} src={StarsBackground} alt="" draggable={false} />
</div> </div>
<h2 className={styles.headerText}> <h2 className={styles.headerText}>
{lang('StarsConfirmPurchaseTitle')} {inviteCustomPeer ? oldLang('StarsSubscribeTitle') : oldLang('StarsConfirmPurchaseTitle')}
</h2> </h2>
<div className={buildClassName(styles.description, styles.smallerText)}> <div className={styles.description}>
{renderText(descriptionText, ['simple_markdown', 'emoji'])} {renderText(descriptionText, ['simple_markdown', 'emoji'])}
</div> </div>
<Button className={styles.paymentButton} size="smaller" onClick={handlePayment} isLoading={isLoading}> <Button className={styles.paymentButton} size="smaller" onClick={handlePayment} isLoading={isLoading}>
{lang('Stars.Transfer.Pay')} {oldLang('Stars.Transfer.Pay')}
<div className={styles.paymentAmount}> <div className={styles.paymentAmount}>
{payment?.invoice?.amount} {payment?.invoice?.amount}
<StarIcon className={styles.paymentButtonStar} size="small" /> <StarIcon className={styles.paymentButtonStar} size="small" />
</div> </div>
</Button> </Button>
{disclaimerText && (
<div className={buildClassName(styles.disclaimer, styles.smallerText)}>
{disclaimerText}
</div>
)}
</Modal> </Modal>
); );
}; };
@ -152,12 +198,17 @@ export default memo(withGlobal<OwnProps>(
const chat = messageInputInvoice ? selectChat(global, messageInputInvoice.chatId) : undefined; const chat = messageInputInvoice ? selectChat(global, messageInputInvoice.chatId) : undefined;
const isPaidMedia = message?.content.paidMedia; const isPaidMedia = message?.content.paidMedia;
const inviteInputInvoice = payment.inputInvoice?.type === 'chatInviteSubscription'
? payment.inputInvoice : undefined;
const inviteInfo = inviteInputInvoice?.inviteInfo;
return { return {
bot, bot,
starsBalanceState: global.stars, starsBalanceState: global.stars,
payment, payment,
paidMediaMessage: isPaidMedia ? message : undefined, paidMediaMessage: isPaidMedia ? message : undefined,
paidMediaChat: isPaidMedia ? chat : undefined, paidMediaChat: isPaidMedia ? chat : undefined,
inviteInfo,
}; };
}, },
)(StarPaymentModal)); )(StarPaymentModal));

View File

@ -0,0 +1,69 @@
@use '../../../../styles/mixins';
.root {
display: flex;
gap: 0.75rem;
padding: 0.25rem 0.75rem 0.25rem 0.25rem;
border-radius: 0.5rem;
cursor: var(--custom-cursor, pointer);
transition: background-color 0.25s ease-out;
&:hover {
background-color: var(--color-chat-hover);
}
}
.info {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.status {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: center;
}
.statusPricing {
display: flex;
align-items: center;
flex-shrink: 0;
gap: 0.25rem;
}
.amount {
font-weight: 500;
}
.title, .description {
margin-bottom: 0;
}
.title {
font-size: 1rem;
}
.description, .statusPeriod, .statusEnded {
font-size: 0.875rem;
color: var(--color-text-secondary);
}
.statusEnded {
color: var(--color-error);
}
.preview {
position: relative;
align-self: flex-start;
}
.subscriptionStar {
position: absolute;
right: 0;
bottom: 0;
z-index: 1;
@include mixins.filter-outline(1px, var(--color-background));
}

View File

@ -0,0 +1,89 @@
import React, { memo } from '../../../../lib/teact/teact';
import { getActions } from '../../../../global';
import type {
ApiStarsSubscription,
} from '../../../../api/types';
import type { GlobalState } from '../../../../global/types';
import { getSenderTitle } from '../../../../global/helpers';
import { selectPeer } from '../../../../global/selectors';
import { formatDateToString } from '../../../../util/dates/dateFormat';
import { formatInteger } from '../../../../util/textFormat';
import useSelector from '../../../../hooks/data/useSelector';
import useLastCallback from '../../../../hooks/useLastCallback';
import useOldLang from '../../../../hooks/useOldLang';
import Avatar from '../../../common/Avatar';
import StarIcon from '../../../common/icons/StarIcon';
import styles from './StarsSubscriptionItem.module.scss';
type OwnProps = {
subscription: ApiStarsSubscription;
};
function selectProvidedPeer(peerId: string) {
return (global: GlobalState) => (
selectPeer(global, peerId)
);
}
const StarsSubscriptionItem = ({ subscription }: OwnProps) => {
const { openStarsSubscriptionModal } = getActions();
const {
peerId, pricing, until, isCancelled,
} = subscription;
const lang = useOldLang();
const peer = useSelector(selectProvidedPeer(peerId))!;
const handleClick = useLastCallback(() => {
openStarsSubscriptionModal({ subscription });
});
if (!peer) {
return undefined;
}
const hasExpired = until < Date.now() / 1000;
const formattedDate = formatDateToString(until * 1000, lang.code, true, 'long');
return (
<div className={styles.root} onClick={handleClick}>
<div className={styles.preview}>
<Avatar size="medium" peer={peer} />
<StarIcon className={styles.subscriptionStar} type="gold" size="small" />
</div>
<div className={styles.info}>
<h3 className={styles.title}>{getSenderTitle(lang, peer)}</h3>
<p className={styles.description}>
{lang(
hasExpired ? 'StarsSubscriptionExpired'
: isCancelled ? 'StarsSubscriptionExpires' : 'StarsSubscriptionRenews',
formattedDate,
)}
</p>
</div>
<div className={styles.status}>
{(isCancelled || hasExpired) ? (
<div className={styles.statusEnded}>
{lang(hasExpired ? 'StarsSubscriptionStatusExpired' : 'StarsSubscriptionStatusCancelled')}
</div>
) : (
<>
<div className={styles.statusPricing}>
<StarIcon className={styles.star} type="gold" size="adaptive" />
<span className={styles.amount}>
{formatInteger(pricing.amount)}
</span>
</div>
<div className={styles.statusPeriod}>{lang('StarsParticipantSubscriptionPerMonth')}</div>
</>
)}
</div>
</div>
);
};
export default memo(StarsSubscriptionItem);

View File

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

View File

@ -0,0 +1,72 @@
@use '../../../../styles/mixins';
.modal {
z-index: calc(var(--z-modal-low-priority) + 1);
}
.header {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
margin-bottom: 1rem;
position: relative;
}
.starsHeader {
gap: normal;
}
.title, .amount {
margin-bottom: 0;
}
.amount {
display: flex;
align-items: center;
text-align: center;
color: var(--color-text-secondary);
}
.footer {
text-align: center;
margin-block: 0.5rem;
color: var(--color-text-secondary);
font-size: 0.875rem;
}
.starsBackground {
position: absolute;
height: 8rem;
top: 0;
left: 50%;
transform: translateX(-50%);
z-index: -1;
}
.avatarWrapper {
position: relative;
}
.subscriptionStar {
position: absolute;
bottom: 0;
right: 0;
font-size: 2rem;
z-index: 1;
@include mixins.filter-outline(2px, var(--color-background));
}
.amountStar {
font-size: 1.25rem;
}
.secondary {
color: var(--color-text-secondary);
}
.danger {
color: var(--color-error);
}

View File

@ -0,0 +1,230 @@
import type { FC } from '../../../../lib/teact/teact';
import React, { memo, useMemo } from '../../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../../global';
import type {
ApiPeer,
} from '../../../../api/types';
import type { TabState } from '../../../../global/types';
import {
selectPeer,
} from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName';
import { formatDateTimeToString } from '../../../../util/dates/dateFormat';
import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback';
import useOldLang from '../../../../hooks/useOldLang';
import usePrevious from '../../../../hooks/usePrevious';
import Avatar from '../../../common/Avatar';
import StarIcon from '../../../common/icons/StarIcon';
import SafeLink from '../../../common/SafeLink';
import Button from '../../../ui/Button';
import TableInfoModal, { type TableData } from '../../common/TableInfoModal';
import styles from './StarsSubscriptionModal.module.scss';
import StarsBackground from '../../../../assets/stars-bg.png';
export type OwnProps = {
modal: TabState['starsSubscriptionModal'];
};
type StateProps = {
peer?: ApiPeer;
};
const StarsSubscriptionModal: FC<OwnProps & StateProps> = ({
modal, peer,
}) => {
const {
closeStarsSubscriptionModal,
fulfillStarsSubscription,
changeStarsSubscription,
checkChatInvite,
loadStarStatus,
} = getActions();
const oldLang = useOldLang();
const lang = useLang();
const { subscription } = modal || {};
const buttonState = useMemo(() => {
if (!subscription) {
return 'hidden';
}
if (subscription.canRefulfill) {
return 'refulfill';
}
const isActive = subscription.until > Date.now() / 1000;
if (isActive && !subscription.isCancelled) {
return 'cancel';
}
if (isActive && subscription.isCancelled) {
return 'renew';
}
if (!isActive) {
return 'restart';
}
return 'ok';
}, [subscription]);
const handleButtonClick = useLastCallback(() => {
if (!subscription) {
return;
}
switch (buttonState) {
case 'refulfill': {
fulfillStarsSubscription({ id: subscription.id });
break;
}
case 'restart': {
checkChatInvite({ hash: subscription.chatInviteHash! });
loadStarStatus();
break;
}
case 'renew': {
changeStarsSubscription({ id: subscription.id, isCancelled: false });
break;
}
case 'cancel': {
changeStarsSubscription({ id: subscription.id, isCancelled: true });
break;
}
}
closeStarsSubscriptionModal();
});
const starModalData = useMemo(() => {
if (!subscription || !peer) {
return undefined;
}
const {
pricing, until, isCancelled, canRefulfill,
} = subscription;
const header = (
<div className={buildClassName(styles.header, styles.starsHeader)}>
<div className={styles.avatarWrapper}>
<Avatar peer={peer} size="jumbo" />
<StarIcon className={styles.subscriptionStar} type="gold" size="adaptive" />
</div>
<img
className={buildClassName(styles.starsBackground)}
src={StarsBackground}
alt=""
draggable={false}
/>
<h1 className={styles.title}>{oldLang('StarsSubscriptionTitle')}</h1>
<p className={styles.amount}>
{lang('StarsPerMonth', {
amount: pricing.amount,
}, {
withNodes: true,
specialReplacement: {
'⭐️': <StarIcon className={styles.amountStar} size="adaptive" type="gold" />,
},
})}
</p>
</div>
);
const tableData: TableData = [];
tableData.push([
oldLang('StarsSubscriptionChannel'),
{ chatId: peer.id },
]);
const hasExpired = until < Date.now() / 1000;
tableData.push([
oldLang(hasExpired ? 'StarsSubscriptionUntilExpired'
: isCancelled ? 'StarsSubscriptionUntilExpires' : 'StarsSubscriptionUntilRenews'),
formatDateTimeToString(until * 1000, oldLang.code, true),
]);
const footerTos = lang('StarsTransactionTOS', {
link: <SafeLink url={lang('StarsTransactionTOSLink')} text={lang('StarsTransactionTOSLinkText')} />,
}, {
withNodes: true,
});
const footer = (
<span className={styles.footer}>
<p className={styles.secondary}>{footerTos}</p>
{isCancelled && (
<p className={styles.danger}>{oldLang('StarsSubscriptionCancelledText')}</p>
)}
{canRefulfill && (
<p className={styles.secondary}>
{oldLang('StarsSubscriptionRefulfillInfo', formatDateTimeToString(until * 1000, oldLang.code, true))}
</p>
)}
{!isCancelled && !canRefulfill && hasExpired && (
<p className={styles.secondary}>
{oldLang('StarsSubscriptionExpiredInfo', formatDateTimeToString(until * 1000, oldLang.code, true))}
</p>
)}
{!isCancelled && !canRefulfill && !hasExpired && (
<p className={styles.secondary}>
{oldLang('StarsSubscriptionCancelInfo', formatDateTimeToString(until * 1000, oldLang.code, true))}
</p>
)}
{buttonState !== 'hidden' && (
<Button
size="smaller"
color={buttonState === 'cancel' ? 'danger' : 'primary'}
isText={buttonState === 'cancel'}
onClick={handleButtonClick}
>
{oldLang(
buttonState === 'cancel' ? 'StarsSubscriptionCancel'
: buttonState === 'refulfill' ? 'StarsSubscriptionRefulfill'
: buttonState === 'restart' ? 'StarsSubscriptionAgain'
: buttonState === 'renew' ? 'StarsSubscriptionRenew' : 'OK',
)}
</Button>
)}
</span>
);
return {
header,
tableData,
footer,
};
}, [buttonState, lang, oldLang, peer, subscription]);
const prevModalData = usePrevious(starModalData);
const renderingModalData = prevModalData || starModalData;
return (
<TableInfoModal
isOpen={Boolean(subscription)}
className={styles.modal}
header={renderingModalData?.header}
tableData={renderingModalData?.tableData}
footer={renderingModalData?.footer}
onClose={closeStarsSubscriptionModal}
/>
);
};
export default memo(withGlobal<OwnProps>(
(global, { modal }): StateProps => {
const peerId = modal?.subscription.peerId;
const peer = peerId ? selectPeer(global, peerId) : undefined;
return {
peer,
};
},
)(StarsSubscriptionModal));

View File

@ -1,7 +1,9 @@
@use '../../../../styles/mixins';
.root { .root {
display: flex; display: flex;
gap: 0.75rem; gap: 0.75rem;
padding: 0.25rem; padding: 0.25rem 0.75rem 0.25rem 0.25rem;
border-radius: 0.5rem; border-radius: 0.5rem;
cursor: var(--custom-cursor, pointer); cursor: var(--custom-cursor, pointer);
transition: background-color 0.25s ease-out; transition: background-color 0.25s ease-out;
@ -28,10 +30,6 @@
font-weight: 500; font-weight: 500;
} }
.star {
margin-top: -0.25rem;
}
.title, .description, .date { .title, .description, .date {
margin-bottom: 0; margin-bottom: 0;
} }
@ -56,3 +54,17 @@
.negative { .negative {
color: var(--color-error); color: var(--color-error);
} }
.preview {
position: relative;
align-self: flex-start;
}
.subscriptionStar {
position: absolute;
right: 0;
bottom: 0;
z-index: 1;
@include mixins.filter-outline(1px, var(--color-background));
}

View File

@ -27,6 +27,7 @@ import styles from './StarsTransactionItem.module.scss';
type OwnProps = { type OwnProps = {
transaction: ApiStarsTransaction; transaction: ApiStarsTransaction;
className?: string;
}; };
function selectOptionalPeer(peerId?: string) { function selectOptionalPeer(peerId?: string) {
@ -35,7 +36,7 @@ function selectOptionalPeer(peerId?: string) {
); );
} }
const StarsTransactionItem = ({ transaction }: OwnProps) => { const StarsTransactionItem = ({ transaction, className }: OwnProps) => {
const { openStarsTransactionModal } = getActions(); const { openStarsTransactionModal } = getActions();
const { const {
date, date,
@ -43,6 +44,7 @@ const StarsTransactionItem = ({ transaction }: OwnProps) => {
photo, photo,
peer: transactionPeer, peer: transactionPeer,
extendedMedia, extendedMedia,
subscriptionPeriod,
} = transaction; } = transaction;
const lang = useOldLang(); const lang = useOldLang();
@ -50,15 +52,13 @@ const StarsTransactionItem = ({ transaction }: OwnProps) => {
const peer = useSelector(selectOptionalPeer(peerId)); const peer = useSelector(selectOptionalPeer(peerId));
const data = useMemo(() => { const data = useMemo(() => {
let title = transaction.title; let title = (() => {
if (transaction.extendedMedia) { if (transaction.extendedMedia) return lang('StarMediaPurchase');
title = lang('StarMediaPurchase'); if (transaction.subscriptionPeriod) return lang('StarSubscriptionPurchase');
} if (transaction.isReaction) return lang('StarsReactionsSent');
if (transaction.isReaction) {
title = lang('StarsReactionsSent');
}
return transaction.title;
})();
let description; let description;
let status: string | undefined; let status: string | undefined;
let avatarPeer: ApiPeer | CustomPeer | undefined; let avatarPeer: ApiPeer | CustomPeer | undefined;
@ -68,7 +68,7 @@ const StarsTransactionItem = ({ transaction }: OwnProps) => {
avatarPeer = peer || CUSTOM_PEER_PREMIUM; avatarPeer = peer || CUSTOM_PEER_PREMIUM;
} else { } else {
const customPeer = buildStarsTransactionCustomPeer(transaction.peer); const customPeer = buildStarsTransactionCustomPeer(transaction.peer);
title = lang(customPeer.titleKey); title = customPeer.title || lang(customPeer.titleKey!);
description = lang(customPeer.subtitleKey!); description = lang(customPeer.subtitleKey!);
avatarPeer = customPeer; avatarPeer = customPeer;
} }
@ -102,9 +102,14 @@ const StarsTransactionItem = ({ transaction }: OwnProps) => {
}); });
return ( return (
<div className={styles.root} onClick={handleClick}> <div className={buildClassName(styles.root, className)} onClick={handleClick}>
{extendedMedia ? <PaidMediaThumb media={extendedMedia} isTransactionPreview /> <div className={styles.preview}>
: <Avatar size="medium" webPhoto={photo} peer={data.avatarPeer} />} {extendedMedia ? <PaidMediaThumb media={extendedMedia} isTransactionPreview />
: <Avatar size="medium" webPhoto={photo} peer={data.avatarPeer} />}
{Boolean(subscriptionPeriod) && (
<StarIcon className={styles.subscriptionStar} type="gold" size="small" />
)}
</div>
<div className={styles.info}> <div className={styles.info}>
<h3 className={styles.title}>{data.title}</h3> <h3 className={styles.title}>{data.title}</h3>
<p className={styles.description}>{data.description}</p> <p className={styles.description}>{data.description}</p>
@ -117,7 +122,7 @@ const StarsTransactionItem = ({ transaction }: OwnProps) => {
<span className={buildClassName(styles.amount, stars < 0 ? styles.negative : styles.positive)}> <span className={buildClassName(styles.amount, stars < 0 ? styles.negative : styles.positive)}>
{formatStarsTransactionAmount(stars)} {formatStarsTransactionAmount(stars)}
</span> </span>
<StarIcon className={styles.star} type="gold" size="big" /> <StarIcon className={styles.star} type="gold" size="adaptive" />
</div> </div>
</div> </div>
); );

View File

@ -9,7 +9,7 @@ import useModuleLoader from '../../../../hooks/useModuleLoader';
const StarsTransactionModalAsync: FC<OwnProps> = (props) => { const StarsTransactionModalAsync: FC<OwnProps> = (props) => {
const { modal } = props; const { modal } = props;
const StarsTransactionModal = useModuleLoader(Bundles.Extra, 'StarsTransactionInfoModal', !modal); const StarsTransactionModal = useModuleLoader(Bundles.Stars, 'StarsTransactionInfoModal', !modal);
// eslint-disable-next-line react/jsx-props-no-spreading // eslint-disable-next-line react/jsx-props-no-spreading
return StarsTransactionModal ? <StarsTransactionModal {...props} /> : undefined; return StarsTransactionModal ? <StarsTransactionModal {...props} /> : undefined;

View File

@ -1,11 +1,9 @@
@use "../../../../styles/mixins";
.modal { .modal {
z-index: calc(var(--z-modal-low-priority) + 1); z-index: calc(var(--z-modal-low-priority) + 1);
} }
.modal :global(.modal-dialog) {
max-width: 26rem !important;
}
.positive { .positive {
color: var(--color-success); color: var(--color-success);
} }
@ -43,6 +41,11 @@
font-family: var(--font-family-monospace); font-family: var(--font-family-monospace);
font-size: 0.875rem; font-size: 0.875rem;
cursor: pointer; cursor: pointer;
overflow: hidden;
white-space: nowrap;
@include mixins.gradient-border-right(3rem, 1rem);
} }
.description { .description {
@ -53,27 +56,31 @@
text-align: center; text-align: center;
margin-block: 0.5rem; margin-block: 0.5rem;
color: var(--color-text-secondary); color: var(--color-text-secondary);
font-size: 0.875rem;
} }
.starsBackground { .starsBackground {
position: absolute; position: absolute;
height: 8rem; height: 8rem;
top: -8.5rem; top: 0;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
} z-index: -1;
.mediaShift {
top: -1.5rem;
} }
.copyIcon { .copyIcon {
position: absolute;
right: 0.25rem;
top: 50%;
transform: translateY(-50%);
margin-inline-start: 0.25rem; margin-inline-start: 0.25rem;
color: var(--color-primary); color: var(--color-primary);
pointer-events: none;
} }
.mediaPreview { .mediaPreview {
margin-bottom: 2rem; margin-block: 1.5rem 1rem;
cursor: var(--custom-cursor, pointer); cursor: var(--custom-cursor, pointer);
} }

View File

@ -24,9 +24,10 @@ import renderText from '../../../common/helpers/renderText';
import useLang from '../../../../hooks/useLang'; import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback'; import useLastCallback from '../../../../hooks/useLastCallback';
import useOldLang from '../../../../hooks/useOldLang'; import useOldLang from '../../../../hooks/useOldLang';
import usePreviousDeprecated from '../../../../hooks/usePreviousDeprecated'; import usePrevious from '../../../../hooks/usePrevious';
import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker'; import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker';
import Avatar from '../../../common/Avatar';
import Icon from '../../../common/icons/Icon'; import Icon from '../../../common/icons/Icon';
import StarIcon from '../../../common/icons/StarIcon'; import StarIcon from '../../../common/icons/StarIcon';
import SafeLink from '../../../common/SafeLink'; import SafeLink from '../../../common/SafeLink';
@ -55,8 +56,6 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
const oldLang = useOldLang(); const oldLang = useOldLang();
const lang = useLang(); const lang = useLang();
const { transaction } = modal || {}; const { transaction } = modal || {};
const isGift = transaction?.isGift;
const isPrizeStars = transaction?.isPrizeStars;
const handleOpenMedia = useLastCallback(() => { const handleOpenMedia = useLastCallback(() => {
const media = transaction?.extendedMedia; const media = transaction?.extendedMedia;
@ -68,22 +67,6 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
}); });
}); });
const animatedStickerData = useMemo(() => {
if (!transaction) {
return undefined;
}
return (
<AnimatedIconFromSticker
key={transaction.id}
sticker={starGiftSticker}
play={canPlayAnimatedEmojis}
noLoop
nonInteractive
/>
);
}, [canPlayAnimatedEmojis, starGiftSticker, transaction]);
const giftEntryAboutText = useMemo(() => { const giftEntryAboutText = useMemo(() => {
const subtitleText = oldLang('lng_credits_box_history_entry_gift_in_about'); const subtitleText = oldLang('lng_credits_box_history_entry_gift_in_about');
const subtitleTextParts = subtitleText.split('{link}'); const subtitleTextParts = subtitleText.split('{link}');
@ -127,24 +110,23 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
return undefined; return undefined;
} }
const { isGift, isPrizeStars, photo } = transaction;
const customPeer = (transaction.peer && transaction.peer.type !== 'peer' const customPeer = (transaction.peer && transaction.peer.type !== 'peer'
&& buildStarsTransactionCustomPeer(transaction.peer)) || undefined; && buildStarsTransactionCustomPeer(transaction.peer)) || undefined;
const peerId = transaction.peer?.type === 'peer' ? transaction.peer.id : undefined; const peerId = transaction.peer?.type === 'peer' ? transaction.peer.id : undefined;
const toName = transaction.peer && oldLang(getStarsPeerTitleKey(transaction.peer)); const toName = transaction.peer && oldLang(getStarsPeerTitleKey(transaction.peer));
let title = transaction.title; const title = (() => {
if (!title && customPeer) { if (transaction.extendedMedia) return oldLang('StarMediaPurchase');
title = oldLang(customPeer.titleKey); if (transaction.subscriptionPeriod) return oldLang('StarSubscriptionPurchase');
} if (transaction.isReaction) return oldLang('StarsReactionsSent');
if (!title && transaction.extendedMedia) { if (customPeer) return customPeer.title || oldLang(customPeer.titleKey!);
title = oldLang('StarMediaPurchase');
}
if (!title && transaction.isReaction) { return transaction.title;
title = oldLang('StarsReactionsSent'); })();
}
const messageLink = peer && transaction.messageId const messageLink = peer && transaction.messageId
? getMessageLink(peer, undefined, transaction.messageId) : undefined; ? getMessageLink(peer, undefined, transaction.messageId) : undefined;
@ -161,6 +143,9 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
const description = transaction.description || (media ? mediaText : undefined); const description = transaction.description || (media ? mediaText : undefined);
const shouldDisplayAvatar = !media && !isGift && !isPrizeStars;
const avatarPeer = !photo ? (peer || customPeer) : undefined;
const header = ( const header = (
<div className={buildClassName(styles.header, styles.starsHeader)}> <div className={buildClassName(styles.header, styles.starsHeader)}>
{media && ( {media && (
@ -170,14 +155,24 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
onClick={handleOpenMedia} onClick={handleOpenMedia}
/> />
)} )}
{(isGift || isPrizeStars) ? animatedStickerData : ( {(isGift || isPrizeStars) && starGiftSticker && (
<img <AnimatedIconFromSticker
className={buildClassName(styles.starsBackground, media && styles.mediaShift)} key={transaction.id}
src={StarsBackground} sticker={starGiftSticker}
alt="" play={canPlayAnimatedEmojis}
draggable={false} noLoop
nonInteractive
/> />
)} )}
{shouldDisplayAvatar && (
<Avatar peer={avatarPeer} webPhoto={photo} size="jumbo" />
)}
<img
className={buildClassName(styles.starsBackground)}
src={StarsBackground}
alt=""
draggable={false}
/>
{title && <h1 className={styles.title}>{title}</h1>} {title && <h1 className={styles.title}>{title}</h1>}
{(isGift || isPrizeStars) && ( {(isGift || isPrizeStars) && (
<h1 className={buildClassName(styles.title, styles.starTitle)}> <h1 className={buildClassName(styles.title, styles.starTitle)}>
@ -223,18 +218,20 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
tableData.push([ tableData.push([
oldLang('Stars.Transaction.Id'), oldLang('Stars.Transaction.Id'),
( (
<span <>
className={styles.tid} <div
onClick={() => { className={styles.tid}
copyTextToClipboard(transaction.id!); onClick={() => {
showNotification({ copyTextToClipboard(transaction.id!);
message: oldLang('StarsTransactionIDCopied'), showNotification({
}); message: oldLang('StarsTransactionIDCopied'),
}} });
> }}
{transaction.id} >
{transaction.id}
</div>
<Icon className={styles.copyIcon} name="copy" /> <Icon className={styles.copyIcon} name="copy" />
</span> </>
), ),
]); ]);
} }
@ -259,13 +256,12 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
header, header,
tableData, tableData,
footer, footer,
avatarPeer: !transaction.photo ? (peer || customPeer) : undefined,
}; };
}, [ }, [
transaction, oldLang, peer, isGift, isPrizeStars, animatedStickerData, giftOutAboutText, giftEntryAboutText, transaction, oldLang, peer, giftOutAboutText, giftEntryAboutText, canPlayAnimatedEmojis, starGiftSticker,
]); ]);
const prevModalData = usePreviousDeprecated(starModalData); const prevModalData = usePrevious(starModalData);
const renderingModalData = prevModalData || starModalData; const renderingModalData = prevModalData || starModalData;
return ( return (
@ -273,13 +269,8 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
isOpen={Boolean(transaction)} isOpen={Boolean(transaction)}
className={styles.modal} className={styles.modal}
header={renderingModalData?.header} header={renderingModalData?.header}
isGift={isGift}
isPrizeStars={isPrizeStars}
tableData={renderingModalData?.tableData} tableData={renderingModalData?.tableData}
footer={renderingModalData?.footer} footer={renderingModalData?.footer}
noHeaderImage={Boolean(transaction?.extendedMedia)}
headerAvatarWebPhoto={transaction?.photo}
headerAvatarPeer={renderingModalData?.avatarPeer}
buttonText={oldLang('OK')} buttonText={oldLang('OK')}
onClose={closeStarsTransactionModal} onClose={closeStarsTransactionModal}
/> />

View File

@ -5,6 +5,7 @@ import { getActions, withGlobal } from '../../../global';
import type { ApiBoost, ApiBoostStatistics, ApiTypePrepaidGiveaway } from '../../../api/types'; import type { ApiBoost, ApiBoostStatistics, ApiTypePrepaidGiveaway } from '../../../api/types';
import type { TabState } from '../../../global/types'; import type { TabState } from '../../../global/types';
import type { CustomPeer } from '../../../types';
import { import {
GIVEAWAY_BOOST_PER_PREMIUM, GIVEAWAY_BOOST_PER_PREMIUM,
@ -17,7 +18,6 @@ import {
} from '../../../global/selectors'; } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { formatDateAtTime } from '../../../util/dates/dateFormat'; import { formatDateAtTime } from '../../../util/dates/dateFormat';
import { CUSTOM_PEER_STAR, CUSTOM_PEER_TO_BE_DISTRIBUTED } from '../../../util/objects/customPeer';
import { formatInteger } from '../../../util/textFormat'; import { formatInteger } from '../../../util/textFormat';
import { getBoostProgressInfo } from '../../common/helpers/boostInfo'; import { getBoostProgressInfo } from '../../common/helpers/boostInfo';
@ -56,6 +56,19 @@ const GIVEAWAY_IMG_LIST: { [key: number]: string } = {
12: GiftRedRound, 12: GiftRedRound,
}; };
const CUSTOM_PEER_STAR_TEMPLATE: Omit<CustomPeer, 'title' | 'titleKey'> = {
isCustomPeer: true,
avatarIcon: 'star',
peerColorId: 1,
};
const CUSTOM_PEER_TO_BE_DISTRIBUTED: CustomPeer = {
isCustomPeer: true,
titleKey: 'BoostingToBeDistributed',
avatarIcon: 'user',
withPremiumGradient: true,
};
const BoostStatistics = ({ const BoostStatistics = ({
boostStatistics, boostStatistics,
isGiveawayAvailable, isGiveawayAvailable,
@ -199,6 +212,18 @@ const BoostStatistics = ({
const renderBoostList = useLastCallback((boost) => { const renderBoostList = useLastCallback((boost) => {
const hasStars = Boolean(boost?.stars); const hasStars = Boolean(boost?.stars);
let customPeer: CustomPeer | undefined;
if (hasStars) {
customPeer = {
...CUSTOM_PEER_STAR_TEMPLATE,
title: lang('Stars', boost.stars),
};
}
if (!boost.userId) {
customPeer = CUSTOM_PEER_TO_BE_DISTRIBUTED;
}
return ( return (
<ListItem <ListItem
className="chat-item-clickable" className="chat-item-clickable"
@ -208,8 +233,7 @@ const BoostStatistics = ({
<PrivateChatInfo <PrivateChatInfo
className={styles.user} className={styles.user}
userId={boost.userId} userId={boost.userId}
customPeer={hasStars ? { ...CUSTOM_PEER_STAR, titleValue: boost.stars } customPeer={customPeer}
: (!boost.userId ? CUSTOM_PEER_TO_BE_DISTRIBUTED : undefined)}
status={lang('BoostExpireOn', formatDateAtTime(lang, boost.expires * 1000))} status={lang('BoostExpireOn', formatDateAtTime(lang, boost.expires * 1000))}
noEmojiStatus noEmojiStatus
forceShowSelf forceShowSelf

View File

@ -29,6 +29,7 @@ type OwnProps = {
noFastList?: boolean; noFastList?: boolean;
cacheBuster?: any; cacheBuster?: any;
beforeChildren?: React.ReactNode; beforeChildren?: React.ReactNode;
scrollContainerClosest?: string;
children: React.ReactNode; children: React.ReactNode;
onLoadMore?: ({ direction }: { direction: LoadMoreDirection; noScroll?: boolean }) => void; onLoadMore?: ({ direction }: { direction: LoadMoreDirection; noScroll?: boolean }) => void;
onScroll?: (e: UIEvent<HTMLDivElement>) => void; onScroll?: (e: UIEvent<HTMLDivElement>) => void;
@ -61,6 +62,7 @@ const InfiniteScroll: FC<OwnProps> = ({
cacheBuster, cacheBuster,
beforeChildren, beforeChildren,
children, children,
scrollContainerClosest,
onLoadMore, onLoadMore,
onScroll, onScroll,
onWheel, onWheel,
@ -100,8 +102,10 @@ const InfiniteScroll: FC<OwnProps> = ({
// Initial preload // Initial preload
useEffect(() => { useEffect(() => {
const container = containerRef.current; const scrollContainer = scrollContainerClosest
if (!loadMoreBackwards || !container) { ? containerRef.current!.closest<HTMLDivElement>(scrollContainerClosest)!
: containerRef.current!;
if (!loadMoreBackwards || !scrollContainer) {
return; return;
} }
@ -110,16 +114,21 @@ const InfiniteScroll: FC<OwnProps> = ({
return; return;
} }
const { scrollHeight, clientHeight } = container; const { scrollHeight, clientHeight } = scrollContainer;
if (clientHeight && scrollHeight < clientHeight) { if (clientHeight && scrollHeight < clientHeight) {
loadMoreBackwards(); loadMoreBackwards();
} }
}, [items, loadMoreBackwards, preloadBackwards]); }, [items, loadMoreBackwards, preloadBackwards, scrollContainerClosest]);
// Restore `scrollTop` after adding items // Restore `scrollTop` after adding items
useLayoutEffect(() => { useLayoutEffect(() => {
const scrollContainer = scrollContainerClosest
? containerRef.current!.closest<HTMLDivElement>(scrollContainerClosest)!
: containerRef.current!;
const container = containerRef.current!;
requestForcedReflow(() => { requestForcedReflow(() => {
const container = containerRef.current!;
const state = stateRef.current; const state = stateRef.current;
state.listItemElements = container.querySelectorAll<HTMLDivElement>(itemSelector); state.listItemElements = container.querySelectorAll<HTMLDivElement>(itemSelector);
@ -127,7 +136,7 @@ const InfiniteScroll: FC<OwnProps> = ({
let newScrollTop: number; let newScrollTop: number;
if (state.currentAnchor && Array.from(state.listItemElements).includes(state.currentAnchor)) { if (state.currentAnchor && Array.from(state.listItemElements).includes(state.currentAnchor)) {
const { scrollTop } = container; const { scrollTop } = scrollContainer;
const newAnchorTop = state.currentAnchor!.getBoundingClientRect().top; const newAnchorTop = state.currentAnchor!.getBoundingClientRect().top;
newScrollTop = scrollTop + (newAnchorTop - state.currentAnchorTop!); newScrollTop = scrollTop + (newAnchorTop - state.currentAnchorTop!);
} else { } else {
@ -142,18 +151,21 @@ const InfiniteScroll: FC<OwnProps> = ({
return undefined; return undefined;
} }
const { scrollTop } = container; const { scrollTop } = scrollContainer;
if (noScrollRestoreOnTop && scrollTop === 0) { if (noScrollRestoreOnTop && scrollTop === 0) {
return undefined; return undefined;
} }
return () => { return () => {
resetScroll(container, newScrollTop); resetScroll(scrollContainer, newScrollTop);
state.isScrollTopJustUpdated = true; state.isScrollTopJustUpdated = true;
}; };
}); });
}, [items, itemSelector, noScrollRestore, noScrollRestoreOnTop, cacheBuster, withAbsolutePositioning]); }, [
items, itemSelector, noScrollRestore, noScrollRestoreOnTop, cacheBuster, withAbsolutePositioning,
scrollContainerClosest,
]);
const handleScroll = useLastCallback((e: UIEvent<HTMLDivElement>) => { const handleScroll = useLastCallback((e: UIEvent<HTMLDivElement>) => {
if (loadMoreForwards && loadMoreBackwards) { if (loadMoreForwards && loadMoreBackwards) {
@ -168,8 +180,10 @@ const InfiniteScroll: FC<OwnProps> = ({
} }
const listLength = listItemElements.length; const listLength = listItemElements.length;
const container = containerRef.current!; const scrollContainer = scrollContainerClosest
const { scrollTop, scrollHeight, offsetHeight } = container; ? containerRef.current!.closest<HTMLDivElement>(scrollContainerClosest)!
: containerRef.current!;
const { scrollTop, scrollHeight, offsetHeight } = scrollContainer;
const top = listLength ? listItemElements[0].offsetTop : 0; const top = listLength ? listItemElements[0].offsetTop : 0;
const isNearTop = scrollTop <= top + sensitiveArea; const isNearTop = scrollTop <= top + sensitiveArea;
const bottom = listLength const bottom = listLength
@ -237,11 +251,25 @@ const InfiniteScroll: FC<OwnProps> = ({
} }
}); });
useLayoutEffect(() => {
const scrollContainer = scrollContainerClosest
? containerRef.current!.closest<HTMLDivElement>(scrollContainerClosest)!
: containerRef.current!;
if (!scrollContainer) return undefined;
const handleNativeScroll = (e: Event) => handleScroll(e as unknown as UIEvent<HTMLDivElement>);
scrollContainer.addEventListener('scroll', handleNativeScroll);
return () => {
scrollContainer.removeEventListener('scroll', handleNativeScroll);
};
}, [handleScroll, scrollContainerClosest]);
return ( return (
<div <div
ref={containerRef} ref={containerRef}
className={className} className={className}
onScroll={handleScroll}
onWheel={onWheel} onWheel={onWheel}
teactFastList={!noFastList && !withAbsolutePositioning} teactFastList={!noFastList && !withAbsolutePositioning}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}

View File

@ -51,7 +51,7 @@ export const MEDIA_PROGRESSIVE_CACHE_DISABLED = false;
export const MEDIA_PROGRESSIVE_CACHE_NAME = 'tt-media-progressive'; export const MEDIA_PROGRESSIVE_CACHE_NAME = 'tt-media-progressive';
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg'; export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg';
export const LANG_CACHE_NAME = 'tt-lang-packs-v42'; export const LANG_CACHE_NAME = 'tt-lang-packs-v43';
export const ASSET_CACHE_NAME = 'tt-assets'; export const ASSET_CACHE_NAME = 'tt-assets';
export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500]; export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500];
export const DATA_BROADCAST_CHANNEL_NAME = 'tt-global'; export const DATA_BROADCAST_CHANNEL_NAME = 'tt-global';

View File

@ -25,6 +25,7 @@ import {
RE_TG_LINK, RE_TG_LINK,
SAVED_FOLDER_ID, SAVED_FOLDER_ID,
SERVICE_NOTIFICATIONS_USER_ID, SERVICE_NOTIFICATIONS_USER_ID,
STARS_CURRENCY_CODE,
TME_WEB_DOMAINS, TME_WEB_DOMAINS,
TMP_CHAT_ID, TMP_CHAT_ID,
TOP_CHAT_MESSAGES_PRELOAD_LIMIT, TOP_CHAT_MESSAGES_PRELOAD_LIMIT,
@ -56,8 +57,10 @@ import {
} from '../../index'; } from '../../index';
import { import {
addChatMembers, addChatMembers,
addChats,
addMessages, addMessages,
addSimilarChannels, addSimilarChannels,
addUsers,
addUserStatuses, addUserStatuses,
deleteChatMessages, deleteChatMessages,
deletePeerPhoto, deletePeerPhoto,
@ -466,7 +469,7 @@ addActionHandler('openThread', async (global, actions, payload): Promise<void> =
}); });
addActionHandler('openLinkedChat', async (global, actions, payload): Promise<void> => { addActionHandler('openLinkedChat', async (global, actions, payload): Promise<void> => {
const { id, tabId = getCurrentTabId() } = payload!; const { id, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, id); const chat = selectChat(global, id);
if (!chat) { if (!chat) {
return; return;
@ -534,7 +537,7 @@ addActionHandler('loadAllChats', async (global, actions, payload): Promise<void>
addActionHandler('loadFullChat', (global, actions, payload): ActionReturnType => { addActionHandler('loadFullChat', (global, actions, payload): ActionReturnType => {
const { const {
chatId, force, withPhotos, chatId, force, withPhotos,
} = payload!; } = payload;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) { if (!chat) {
return; return;
@ -716,7 +719,7 @@ addActionHandler('createChannel', async (global, actions, payload): Promise<void
}); });
addActionHandler('joinChannel', async (global, actions, payload): Promise<void> => { addActionHandler('joinChannel', async (global, actions, payload): Promise<void> => {
const { chatId, tabId = getCurrentTabId() } = payload!; const { chatId, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) { if (!chat) {
return; return;
@ -758,7 +761,7 @@ addActionHandler('deleteChatUser', (global, actions, payload): ActionReturnType
}); });
addActionHandler('deleteChat', (global, actions, payload): ActionReturnType => { addActionHandler('deleteChat', (global, actions, payload): ActionReturnType => {
const { chatId, tabId = getCurrentTabId() } = payload!; const { chatId, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) { if (!chat) {
return; return;
@ -775,7 +778,7 @@ addActionHandler('deleteChat', (global, actions, payload): ActionReturnType => {
}); });
addActionHandler('leaveChannel', async (global, actions, payload): Promise<void> => { addActionHandler('leaveChannel', async (global, actions, payload): Promise<void> => {
const { chatId, tabId = getCurrentTabId() } = payload!; const { chatId, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) { if (!chat) {
return; return;
@ -797,8 +800,6 @@ addActionHandler('leaveChannel', async (global, actions, payload): Promise<void>
global = deleteChatMessages(global, chatId, localMessageIds); global = deleteChatMessages(global, chatId, localMessageIds);
setGlobal(global); setGlobal(global);
} }
actions.loadFullChat({ chatId, force: true });
}); });
addActionHandler('deleteChannel', (global, actions, payload): ActionReturnType => { addActionHandler('deleteChannel', (global, actions, payload): ActionReturnType => {
@ -891,7 +892,7 @@ addActionHandler('createGroupChat', async (global, actions, payload): Promise<vo
}); });
addActionHandler('toggleChatPinned', (global, actions, payload): ActionReturnType => { addActionHandler('toggleChatPinned', (global, actions, payload): ActionReturnType => {
const { id, folderId, tabId = getCurrentTabId() } = payload!; const { id, folderId, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, id); const chat = selectChat(global, id);
if (!chat) { if (!chat) {
return; return;
@ -938,7 +939,7 @@ addActionHandler('toggleChatPinned', (global, actions, payload): ActionReturnTyp
}); });
addActionHandler('toggleChatArchived', (global, actions, payload): ActionReturnType => { addActionHandler('toggleChatArchived', (global, actions, payload): ActionReturnType => {
const { id } = payload!; const { id } = payload;
const chat = selectChat(global, id); const chat = selectChat(global, id);
if (chat) { if (chat) {
void callApi('toggleChatArchived', { void callApi('toggleChatArchived', {
@ -949,7 +950,7 @@ addActionHandler('toggleChatArchived', (global, actions, payload): ActionReturnT
}); });
addActionHandler('toggleSavedDialogPinned', (global, actions, payload): ActionReturnType => { addActionHandler('toggleSavedDialogPinned', (global, actions, payload): ActionReturnType => {
const { id, tabId = getCurrentTabId() } = payload!; const { id, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, id); const chat = selectChat(global, id);
if (!chat) { if (!chat) {
return; return;
@ -1046,7 +1047,7 @@ addActionHandler('editChatFolders', (global, actions, payload): ActionReturnType
}); });
addActionHandler('editChatFolder', (global, actions, payload): ActionReturnType => { addActionHandler('editChatFolder', (global, actions, payload): ActionReturnType => {
const { id, folderUpdate } = payload!; const { id, folderUpdate } = payload;
const folder = selectChatFolder(global, id); const folder = selectChatFolder(global, id);
if (folder) { if (folder) {
@ -1063,7 +1064,7 @@ addActionHandler('editChatFolder', (global, actions, payload): ActionReturnType
}); });
addActionHandler('addChatFolder', async (global, actions, payload): Promise<void> => { addActionHandler('addChatFolder', async (global, actions, payload): Promise<void> => {
const { folder, tabId = getCurrentTabId() } = payload!; const { folder, tabId = getCurrentTabId() } = payload;
const { orderedIds, byId } = global.chatFolders; const { orderedIds, byId } = global.chatFolders;
const limit = selectCurrentLimit(global, 'dialogFilters'); const limit = selectCurrentLimit(global, 'dialogFilters');
@ -1125,7 +1126,7 @@ addActionHandler('addChatFolder', async (global, actions, payload): Promise<void
}); });
addActionHandler('sortChatFolders', async (global, actions, payload): Promise<void> => { addActionHandler('sortChatFolders', async (global, actions, payload): Promise<void> => {
const { folderIds } = payload!; const { folderIds } = payload;
const result = await callApi('sortChatFolders', folderIds); const result = await callApi('sortChatFolders', folderIds);
if (result) { if (result) {
@ -1191,21 +1192,65 @@ addActionHandler('markTopicRead', (global, actions, payload): ActionReturnType =
setGlobal(global); setGlobal(global);
}); });
addActionHandler('openChatByInvite', async (global, actions, payload): Promise<void> => { addActionHandler('checkChatInvite', async (global, actions, payload): Promise<void> => {
const { hash, tabId = getCurrentTabId() } = payload!; const { hash, tabId = getCurrentTabId() } = payload;
const result = await callApi('openChatByInvite', hash); const result = await callApi('checkChatInvite', hash);
if (!result) { if (!result) {
return; return;
} }
actions.openChat({ id: result.chatId, tabId }); global = getGlobal();
if (result.users) {
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
}
if (result.chat) {
global = addChats(global, buildCollectionByKey([result.chat], 'id'));
setGlobal(global);
actions.openChat({ id: result.chat.id, tabId });
return;
}
if (result.invite.subscriptionFormId) {
global = updateTabState(global, {
payment: {
formId: result.invite.subscriptionFormId,
inputInvoice: {
type: 'chatInviteSubscription',
hash,
inviteInfo: result.invite,
},
invoice: {
amount: result.invite.subscriptionPricing!.amount,
currency: STARS_CURRENCY_CODE,
isRecurring: true,
mediaType: 'invoice',
// Placeholder values
title: 'Subscription',
text: '',
},
},
isStarPaymentModalOpen: true,
}, tabId);
setGlobal(global);
return;
}
global = updateTabState(global, {
chatInviteModal: {
hash,
inviteInfo: result.invite,
},
}, tabId);
setGlobal(global);
}); });
addActionHandler('openChatByPhoneNumber', async (global, actions, payload): Promise<void> => { addActionHandler('openChatByPhoneNumber', async (global, actions, payload): Promise<void> => {
const { const {
phoneNumber, startAttach, attach, text, tabId = getCurrentTabId(), phoneNumber, startAttach, attach, text, tabId = getCurrentTabId(),
} = payload!; } = payload;
// Open temporary empty chat to make the click response feel faster // Open temporary empty chat to make the click response feel faster
actions.openChat({ id: TMP_CHAT_ID, tabId }); actions.openChat({ id: TMP_CHAT_ID, tabId });
@ -1240,7 +1285,7 @@ addActionHandler('openTelegramLink', async (global, actions, payload): Promise<v
const { const {
openChatByPhoneNumber, openChatByPhoneNumber,
openChatByInvite, checkChatInvite,
openStickerSet, openStickerSet,
openChatWithDraft, openChatWithDraft,
joinVoiceChatByLink, joinVoiceChatByLink,
@ -1313,7 +1358,7 @@ addActionHandler('openTelegramLink', async (global, actions, payload): Promise<v
} }
if (hash) { if (hash) {
openChatByInvite({ hash, tabId }); checkChatInvite({ hash, tabId });
return; return;
} }
@ -1453,8 +1498,8 @@ addActionHandler('processBoostParameters', async (global, actions, payload): Pro
}); });
}); });
addActionHandler('acceptInviteConfirmation', async (global, actions, payload): Promise<void> => { addActionHandler('acceptChatInvite', async (global, actions, payload): Promise<void> => {
const { hash, tabId = getCurrentTabId() } = payload!; const { hash, tabId = getCurrentTabId() } = payload;
const result = await callApi('importChatInvite', { hash }); const result = await callApi('importChatInvite', { hash });
if (!result) { if (!result) {
return; return;
@ -1467,7 +1512,7 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
const { const {
username, messageId, commentId, startParam, startAttach, attach, threadId, originalParts, startApp, text, username, messageId, commentId, startParam, startAttach, attach, threadId, originalParts, startApp, text,
tabId = getCurrentTabId(), tabId = getCurrentTabId(),
} = payload!; } = payload;
const chat = selectCurrentChat(global, tabId); const chat = selectCurrentChat(global, tabId);
const webAppName = originalParts?.[1]; const webAppName = originalParts?.[1];
@ -1557,7 +1602,7 @@ addActionHandler('togglePreHistoryHidden', async (global, actions, payload): Pro
const { const {
chatId, isEnabled, chatId, isEnabled,
tabId = getCurrentTabId(), tabId = getCurrentTabId(),
} = payload!; } = payload;
const chat = await ensureIsSuperGroup(global, actions, chatId, tabId); const chat = await ensureIsSuperGroup(global, actions, chatId, tabId);
if (!chat) { if (!chat) {
@ -1572,7 +1617,7 @@ addActionHandler('togglePreHistoryHidden', async (global, actions, payload): Pro
}); });
addActionHandler('updateChatDefaultBannedRights', (global, actions, payload): ActionReturnType => { addActionHandler('updateChatDefaultBannedRights', (global, actions, payload): ActionReturnType => {
const { chatId, bannedRights } = payload!; const { chatId, bannedRights } = payload;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) { if (!chat) {
@ -1586,7 +1631,7 @@ addActionHandler('updateChatMemberBannedRights', async (global, actions, payload
const { const {
chatId, userId, bannedRights, chatId, userId, bannedRights,
tabId = getCurrentTabId(), tabId = getCurrentTabId(),
} = payload!; } = payload;
const user = selectUser(global, userId); const user = selectUser(global, userId);
@ -1634,7 +1679,7 @@ addActionHandler('updateChatAdmin', async (global, actions, payload): Promise<vo
const { const {
chatId, userId, adminRights, customTitle, chatId, userId, adminRights, customTitle,
tabId = getCurrentTabId(), tabId = getCurrentTabId(),
} = payload!; } = payload;
const user = selectUser(global, userId); const user = selectUser(global, userId);
if (!user) { if (!user) {

View File

@ -14,10 +14,10 @@ import { isChatChannel, isChatSuperGroup } from '../../helpers';
import { import {
getPrizeStarsTransactionFromGiveaway, getPrizeStarsTransactionFromGiveaway,
getRequestInputInvoice, getRequestInputInvoice,
getStarsTransactionFromGift,
} from '../../helpers/payments'; } from '../../helpers/payments';
import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { addActionHandler, getGlobal, setGlobal } from '../../index';
import { import {
appendStarsSubscriptions,
appendStarsTransactions, closeInvoice, appendStarsTransactions, closeInvoice,
openStarsTransactionFromReceipt, openStarsTransactionFromReceipt,
openStarsTransactionModal, openStarsTransactionModal,
@ -38,6 +38,7 @@ import {
selectChatMessage, selectChatMessage,
selectPaymentFormId, selectPaymentFormId,
selectPaymentInputInvoice, selectPaymentRequestId, selectPaymentInputInvoice, selectPaymentRequestId,
selectPeer,
selectProviderPublicToken, selectProviderPublicToken,
selectProviderPublishableKey, selectProviderPublishableKey,
selectSmartGlocalCredentials, selectSmartGlocalCredentials,
@ -271,6 +272,10 @@ addActionHandler('sendStarPaymentForm', async (global, actions, payload): Promis
global = closeInvoice(global, tabId); global = closeInvoice(global, tabId);
setGlobal(global); setGlobal(global);
if ('channelId' in result) {
actions.openChat({ id: result.channelId, tabId });
}
actions.apiUpdate({ actions.apiUpdate({
'@type': 'updatePaymentStateCompleted', '@type': 'updatePaymentStateCompleted',
inputInvoice, inputInvoice,
@ -535,22 +540,6 @@ addActionHandler('closeStarsGiftingModal', (global, actions, payload): ActionRet
}, tabId); }, tabId);
}); });
addActionHandler('openStarsTransactionFromGift', (global, actions, payload): ActionReturnType => {
const {
chatId,
messageId,
tabId = getCurrentTabId(),
} = payload || {};
const message = selectChatMessage(global, chatId, messageId);
if (!message) return undefined;
const transaction = getStarsTransactionFromGift(message);
if (!transaction) return undefined;
return openStarsTransactionModal(global, transaction, tabId);
});
addActionHandler('openPrizeStarsTransactionFromGiveaway', (global, actions, payload): ActionReturnType => { addActionHandler('openPrizeStarsTransactionFromGiveaway', (global, actions, payload): ActionReturnType => {
const { const {
chatId, chatId,
@ -1056,11 +1045,18 @@ addActionHandler('loadStarStatus', async (global): Promise<void> => {
inbound: undefined, inbound: undefined,
outbound: undefined, outbound: undefined,
}, },
subscriptions: undefined,
}, },
}; };
if (status.history) { if (status.history) {
global = appendStarsTransactions(global, 'all', status.history, status.nextOffset); global = appendStarsTransactions(global, 'all', status.history, status.nextHistoryOffset);
} }
if (status.subscriptions) {
global = appendStarsSubscriptions(global, status.subscriptions, status.nextSubscriptionOffset);
}
setGlobal(global); setGlobal(global);
}); });
@ -1089,3 +1085,54 @@ addActionHandler('loadStarsTransactions', async (global, actions, payload): Prom
} }
setGlobal(global); setGlobal(global);
}); });
addActionHandler('loadStarsSubscriptions', async (global): Promise<void> => {
const subscriptions = global.stars?.subscriptions;
const offset = subscriptions?.nextOffset;
if (subscriptions && !offset) return; // Already loaded all
const result = await callApi('fetchStarsSubscriptions', {
offset: offset || '',
});
if (!result) {
return;
}
global = getGlobal();
global = updateStarsBalance(global, result.balance);
global = appendStarsSubscriptions(global, result.subscriptions, result.nextOffset);
setGlobal(global);
});
addActionHandler('changeStarsSubscription', async (global, actions, payload): Promise<void> => {
const { peerId, id, isCancelled } = payload;
const peer = peerId ? selectPeer(global, peerId) : undefined;
if (peerId && !peer) return;
await callApi('changeStarsSubscription', {
peer,
subscriptionId: id,
isCancelled,
});
actions.loadStarStatus();
});
addActionHandler('fulfillStarsSubscription', async (global, actions, payload): Promise<void> => {
const { peerId, id } = payload;
const peer = peerId ? selectPeer(global, peerId) : undefined;
if (peerId && !peer) return;
await callApi('fulfillStarsSubscription', {
peer,
subscriptionId: id,
});
actions.loadStarStatus();
});

View File

@ -125,8 +125,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
const chat = selectChat(global, update.id); const chat = selectChat(global, update.id);
if (chat && isChatChannel(chat)) { if (chat && isChatChannel(chat)) {
const chatMessages = selectChatMessages(global, update.id); const chatMessages = selectChatMessages(global, update.id);
const localMessageIds = Object.keys(chatMessages).map(Number).filter(isLocalMessageId); if (chatMessages) {
global = deleteChatMessages(global, chat.id, localMessageIds); const localMessageIds = Object.keys(chatMessages).map(Number).filter(isLocalMessageId);
global = deleteChatMessages(global, chat.id, localMessageIds);
}
} }
return global; return global;
@ -434,16 +436,6 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
return global; return global;
} }
case 'showInvite': {
const { data } = update;
Object.values(global.byTabId).forEach(({ id: tabId }) => {
actions.showDialog({ data, tabId });
});
return undefined;
}
case 'updatePendingJoinRequests': { case 'updatePendingJoinRequests': {
const { chatId, requestsPending, recentRequesterIds } = update; const { chatId, requestsPending, recentRequesterIds } = update;
const chat = global.chats.byId[chatId]; const chat = global.chats.byId[chatId];

View File

@ -176,7 +176,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
} }
case 'updatePaidReactionPrivacy': { case 'updatePaidReactionPrivacy': {
return { global = {
...global, ...global,
settings: { settings: {
...global.settings, ...global.settings,

View File

@ -1,5 +1,6 @@
import type { ActionReturnType } from '../../types'; import type { ActionReturnType } from '../../types';
import { STARS_CURRENCY_CODE } from '../../../config';
import { areDeepEqual } from '../../../util/areDeepEqual'; import { areDeepEqual } from '../../../util/areDeepEqual';
import { formatCurrencyAsString } from '../../../util/formatCurrency'; import { formatCurrencyAsString } from '../../../util/formatCurrency';
import * as langProvider from '../../../util/oldLangProvider'; import * as langProvider from '../../../util/oldLangProvider';
@ -19,13 +20,15 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
if (invoice) { if (invoice) {
const { amount, currency, title } = invoice; const { amount, currency, title } = invoice;
actions.showNotification({ if (currency !== STARS_CURRENCY_CODE) {
tabId, actions.showNotification({
message: langProvider.oldTranslate('PaymentInfoHint', [ tabId,
formatCurrencyAsString(amount, currency, langProvider.getTranslationFn().code), message: langProvider.oldTranslate('PaymentInfoHint', [
title, formatCurrencyAsString(amount, currency, langProvider.getTranslationFn().code),
]), title,
}); ]),
});
}
} }
if (inputInvoice?.type === 'giftcode') { if (inputInvoice?.type === 'giftcode') {
@ -42,7 +45,6 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
isCompleted: true, isCompleted: true,
}, },
}, tabId); }, tabId);
global = closeInvoice(global, tabId);
} }
} }
@ -60,14 +62,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
isCompleted: true, isCompleted: true,
}, },
}, tabId); }, tabId);
global = closeInvoice(global, tabId);
} }
} }
if (inputInvoice?.type === 'stars') { if (inputInvoice?.type === 'stars') {
if (!inputInvoice.stars) {
return;
}
const starsModalState = selectTabState(global, tabId).starsGiftModal; const starsModalState = selectTabState(global, tabId).starsGiftModal;
if (starsModalState && starsModalState.isOpen) { if (starsModalState && starsModalState.isOpen) {
@ -77,10 +75,27 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
isCompleted: true, isCompleted: true,
}, },
}, tabId); }, tabId);
global = closeInvoice(global, tabId);
} }
actions.loadStarStatus(); // Manually reload. Server update takes ~10 seconds
} }
if (inputInvoice?.type === 'chatInviteSubscription') {
const { amount } = invoice!;
actions.showNotification({
tabId,
title: langProvider.oldTranslate('StarsSubscriptionCompleted'),
message: langProvider.oldTranslate('StarsSubscriptionCompletedText', [
amount,
inputInvoice.inviteInfo.title,
], undefined, amount),
icon: 'star',
});
}
if (invoice?.currency === STARS_CURRENCY_CODE) {
global = closeInvoice(global, tabId);
}
setGlobal(global); setGlobal(global);
}); });

View File

@ -204,3 +204,10 @@ addActionHandler('requestChatTranslation', (global, actions, payload): ActionRet
const { chatId, toLanguageCode, tabId = getCurrentTabId() } = payload; const { chatId, toLanguageCode, tabId = getCurrentTabId() } = payload;
return updateRequestedChatTranslation(global, chatId, toLanguageCode, tabId); return updateRequestedChatTranslation(global, chatId, toLanguageCode, tabId);
}); });
addActionHandler('closeChatInviteModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
chatInviteModal: undefined,
}, tabId);
});

View File

@ -1,12 +1,13 @@
import type { ActionReturnType } from '../../types'; import type { ActionReturnType } from '../../types';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { getStarsTransactionFromGift } from '../../helpers/payments';
import { addActionHandler } from '../../index'; import { addActionHandler } from '../../index';
import { import {
clearPayment, closeInvoice, openStarsTransactionModal, updatePayment, clearPayment, closeInvoice, openStarsTransactionModal, updatePayment,
} from '../../reducers'; } from '../../reducers';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { selectTabState } from '../../selectors'; import { selectChatMessage, selectTabState } from '../../selectors';
addActionHandler('closePaymentModal', (global, actions, payload): ActionReturnType => { addActionHandler('closePaymentModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {}; const { tabId = getCurrentTabId() } = payload || {};
@ -97,6 +98,22 @@ addActionHandler('openStarsTransactionModal', (global, actions, payload): Action
return openStarsTransactionModal(global, transaction, tabId); return openStarsTransactionModal(global, transaction, tabId);
}); });
addActionHandler('openStarsTransactionFromGift', (global, actions, payload): ActionReturnType => {
const {
chatId,
messageId,
tabId = getCurrentTabId(),
} = payload || {};
const message = selectChatMessage(global, chatId, messageId);
if (!message) return undefined;
const transaction = getStarsTransactionFromGift(message);
if (!transaction) return undefined;
return openStarsTransactionModal(global, transaction, tabId);
});
addActionHandler('closeStarsTransactionModal', (global, actions, payload): ActionReturnType => { addActionHandler('closeStarsTransactionModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {}; const { tabId = getCurrentTabId() } = payload || {};
@ -104,3 +121,21 @@ addActionHandler('closeStarsTransactionModal', (global, actions, payload): Actio
starsTransactionModal: undefined, starsTransactionModal: undefined,
}, tabId); }, tabId);
}); });
addActionHandler('openStarsSubscriptionModal', (global, actions, payload): ActionReturnType => {
const { subscription, tabId = getCurrentTabId() } = payload;
return updateTabState(global, {
starsSubscriptionModal: {
subscription,
},
}, tabId);
});
addActionHandler('closeStarsSubscriptionModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
starsSubscriptionModal: undefined,
}, tabId);
});

View File

@ -4,12 +4,15 @@ import type {
ApiChatBannedRights, ApiChatBannedRights,
ApiChatFolder, ApiChatFolder,
ApiChatFullInfo, ApiChatFullInfo,
ApiChatInviteInfo,
ApiPeer, ApiPeer,
ApiTopic, ApiTopic,
ApiUser, ApiUser,
} from '../../api/types'; } from '../../api/types';
import type { LangFn } from '../../hooks/useOldLang'; import type { LangFn } from '../../hooks/useOldLang';
import type { NotifyException, NotifySettings, ThreadId } from '../../types'; import type {
CustomPeer, NotifyException, NotifySettings, ThreadId,
} from '../../types';
import { MAIN_THREAD_ID } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types';
import { import {
@ -482,3 +485,16 @@ export function getGroupStatus(lang: LangFn, chat: ApiChat) {
? lang('Subscribers', membersCount, 'i') ? lang('Subscribers', membersCount, 'i')
: lang('Members', membersCount, 'i'); : lang('Members', membersCount, 'i');
} }
export function getCustomPeerFromInvite(invite: ApiChatInviteInfo): CustomPeer {
const {
title, color, isVerified, isFake, isScam,
} = invite;
return {
isCustomPeer: true,
title,
peerColorId: color,
isVerified,
fakeType: isFake ? 'fake' : isScam ? 'scam' : undefined,
};
}

View File

@ -53,6 +53,15 @@ export function getRequestInputInvoice<T extends GlobalState>(
}; };
} }
if (inputInvoice.type === 'chatInviteSubscription') {
const { hash } = inputInvoice;
return {
type: 'chatInviteSubscription',
hash,
};
}
if (inputInvoice.type === 'message') { if (inputInvoice.type === 'message') {
const chat = selectChat(global, inputInvoice.chatId); const chat = selectChat(global, inputInvoice.chatId);
if (!chat) { if (!chat) {

View File

@ -385,6 +385,7 @@ export function leaveChat<T extends GlobalState>(global: T, leftChatId: string):
global = removeChatFromChatLists(global, leftChatId); global = removeChatFromChatLists(global, leftChatId);
global = updateChat(global, leftChatId, { isNotJoined: true }); global = updateChat(global, leftChatId, { isNotJoined: true });
global = updateChatFullInfo(global, leftChatId, { joinInfo: undefined });
return global; return global;
} }

View File

@ -2,6 +2,7 @@ import type {
ApiInvoice, ApiPaymentForm, ApiInvoice, ApiPaymentForm,
ApiReceiptRegular, ApiReceiptRegular,
ApiReceiptStars, ApiReceiptStars,
ApiStarsSubscription,
ApiStarsTransaction, ApiStarsTransaction,
} from '../../api/types'; } from '../../api/types';
import type { PaymentStep, ShippingOption } from '../../types'; import type { PaymentStep, ShippingOption } from '../../types';
@ -187,6 +188,29 @@ export function appendStarsTransactions<T extends GlobalState>(
}; };
} }
export function appendStarsSubscriptions<T extends GlobalState>(
global: T,
subscriptions: ApiStarsSubscription[],
nextOffset?: string,
): T {
if (!global.stars) {
return global;
}
const newObject = {
list: (global.stars.subscriptions?.list || []).concat(subscriptions),
nextOffset,
};
return {
...global,
stars: {
...global.stars,
subscriptions: newObject,
},
};
}
export function openStarsTransactionModal<T extends GlobalState>( export function openStarsTransactionModal<T extends GlobalState>(
global: T, transaction: ApiStarsTransaction, ...[tabId = getCurrentTabId()]: TabArgs<T> global: T, transaction: ApiStarsTransaction, ...[tabId = getCurrentTabId()]: TabArgs<T>
): T { ): T {

View File

@ -14,6 +14,7 @@ import type {
ApiChatBannedRights, ApiChatBannedRights,
ApiChatFolder, ApiChatFolder,
ApiChatFullInfo, ApiChatFullInfo,
ApiChatInviteInfo,
ApiChatlistExportedInvite, ApiChatlistExportedInvite,
ApiChatlistInvite, ApiChatlistInvite,
ApiChatReactions, ApiChatReactions,
@ -33,7 +34,6 @@ import type {
ApiGroupStatistics, ApiGroupStatistics,
ApiInputInvoice, ApiInputInvoice,
ApiInputMessageReplyInfo, ApiInputMessageReplyInfo,
ApiInviteInfo,
ApiInvoice, ApiInvoice,
ApiKeyboardButton, ApiKeyboardButton,
ApiMediaFormat, ApiMediaFormat,
@ -65,6 +65,7 @@ import type {
ApiSession, ApiSession,
ApiSessionData, ApiSessionData,
ApiSponsoredMessage, ApiStarGiveawayOption, ApiSponsoredMessage, ApiStarGiveawayOption,
ApiStarsSubscription,
ApiStarsTransaction, ApiStarsTransaction,
ApiStarTopupOption, ApiStarTopupOption,
ApiStealthMode, ApiStealthMode,
@ -183,6 +184,10 @@ export type StarsTransactionHistory = Record<StarsTransactionType, {
transactions: ApiStarsTransaction[]; transactions: ApiStarsTransaction[];
nextOffset?: string; nextOffset?: string;
} | undefined>; } | undefined>;
export type StarsSubscriptions = {
list: ApiStarsSubscription[];
nextOffset?: string;
};
export type ConfettiStyle = 'poppers' | 'top-down'; export type ConfettiStyle = 'poppers' | 'top-down';
@ -351,6 +356,11 @@ export type TabState = {
messageIds: number[]; messageIds: number[];
}; };
chatInviteModal?: {
hash: string;
inviteInfo: ApiChatInviteInfo;
};
seenByModal?: { seenByModal?: {
chatId: string; chatId: string;
messageId: number; messageId: number;
@ -595,7 +605,7 @@ export type TabState = {
}; };
notifications: ApiNotification[]; notifications: ApiNotification[];
dialogs: (ApiError | ApiInviteInfo | ApiContact)[]; dialogs: (ApiError | ApiContact)[];
safeLinkModalUrl?: string; safeLinkModalUrl?: string;
mapModal?: { mapModal?: {
@ -748,6 +758,9 @@ export type TabState = {
starsTransactionModal?: { starsTransactionModal?: {
transaction: ApiStarsTransaction; transaction: ApiStarsTransaction;
}; };
starsSubscriptionModal?: {
subscription: ApiStarsSubscription;
};
giftModal?: { giftModal?: {
isCompleted?: boolean; isCompleted?: boolean;
@ -1216,6 +1229,7 @@ export type GlobalState = {
topupOptions: ApiStarTopupOption[]; topupOptions: ApiStarTopupOption[];
balance: number; balance: number;
history: StarsTransactionHistory; history: StarsTransactionHistory;
subscriptions?: StarsSubscriptions;
}; };
}; };
@ -1337,7 +1351,12 @@ export interface ActionPayloads {
adminRights: ApiChatAdminRights; adminRights: ApiChatAdminRights;
customTitle?: string; customTitle?: string;
} & WithTabId; } & WithTabId;
acceptInviteConfirmation: { hash: string } & WithTabId;
checkChatInvite: {
hash: string;
} & WithTabId;
acceptChatInvite: { hash: string } & WithTabId;
closeChatInviteModal: WithTabId | undefined;
// settings // settings
setSettingOption: Partial<ISettings> | undefined; setSettingOption: Partial<ISettings> | undefined;
@ -1546,9 +1565,6 @@ export interface ActionPayloads {
attach?: string; attach?: string;
text?: string; text?: string;
} & WithTabId; } & WithTabId;
openChatByInvite: {
hash: string;
} & WithTabId;
toggleSavedDialogPinned: { toggleSavedDialogPinned: {
id: string; id: string;
} & WithTabId; } & WithTabId;
@ -1839,6 +1855,10 @@ export interface ActionPayloads {
messageId: number; messageId: number;
} & WithTabId; } & WithTabId;
closeStarsTransactionModal: WithTabId | undefined; closeStarsTransactionModal: WithTabId | undefined;
openStarsSubscriptionModal: {
subscription: ApiStarsSubscription;
} & WithTabId;
closeStarsSubscriptionModal: WithTabId | undefined;
openPrizeStarsTransactionFromGiveaway: { openPrizeStarsTransactionFromGiveaway: {
chatId: string; chatId: string;
messageId: number; messageId: number;
@ -2304,6 +2324,16 @@ export interface ActionPayloads {
loadStarsTransactions: { loadStarsTransactions: {
type: StarsTransactionType; type: StarsTransactionType;
}; };
loadStarsSubscriptions: undefined;
changeStarsSubscription: {
peerId?: string;
id: string;
isCancelled: boolean;
};
fulfillStarsSubscription: {
peerId?: string;
id: string;
};
openStarsBalanceModal: { openStarsBalanceModal: {
originPayment?: TabState['payment']; originPayment?: TabState['payment'];
originReaction?: { originReaction?: {

View File

@ -1000,8 +1000,11 @@ class TelegramClient {
if (isExported) this.releaseExportedSender(sender); if (isExported) this.releaseExportedSender(sender);
return result; return result;
} catch (e) { } catch (e) {
if (e instanceof errors.ServerError || e.message === 'RPC_CALL_FAIL' if (e instanceof errors.ServerError
|| e.message === 'RPC_MCGET_FAIL') { || e.message === 'RPC_CALL_FAIL'
|| e.message === 'RPC_MCGET_FAIL'
|| e.message.match(/INTERDC_\d_CALL(_RICH)?_ERROR/)
) {
this._log.warn(`Telegram is having internal issues ${e.constructor.name}`); this._log.warn(`Telegram is having internal issues ${e.constructor.name}`);
await sleep(2000); await sleep(2000);
} else if (e instanceof errors.FloodWaitError || e instanceof errors.FloodTestPhoneWaitError) { } else if (e instanceof errors.FloodWaitError || e instanceof errors.FloodTestPhoneWaitError) {

View File

@ -1654,6 +1654,9 @@ payments.sendStarsForm#2bb731d flags:# form_id:long invoice:InputInvoice = payme
payments.refundStarsCharge#25ae8f4a user_id:InputUser charge_id:string = Updates; payments.refundStarsCharge#25ae8f4a user_id:InputUser charge_id:string = Updates;
payments.getStarsTransactionsByID#27842d2e peer:InputPeer id:Vector<InputStarsTransaction> = payments.StarsStatus; payments.getStarsTransactionsByID#27842d2e peer:InputPeer id:Vector<InputStarsTransaction> = payments.StarsStatus;
payments.getStarsGiftOptions#d3c96bc8 flags:# user_id:flags.0?InputUser = Vector<StarsGiftOption>; payments.getStarsGiftOptions#d3c96bc8 flags:# user_id:flags.0?InputUser = Vector<StarsGiftOption>;
payments.getStarsSubscriptions#32512c5 flags:# missing_balance:flags.0?true peer:InputPeer offset:string = payments.StarsStatus;
payments.changeStarsSubscription#c7770878 flags:# peer:InputPeer subscription_id:string canceled:flags.0?Bool = Bool;
payments.fulfillStarsSubscription#cc5bebb3 peer:InputPeer subscription_id:string = Bool;
payments.getStarsGiveawayOptions#bd1efd3e = Vector<StarsGiveawayOption>; payments.getStarsGiveawayOptions#bd1efd3e = Vector<StarsGiveawayOption>;
phone.requestCall#42ff96ed flags:# video:flags.0?true user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall; phone.requestCall#42ff96ed flags:# video:flags.0?true user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
phone.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall; phone.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall;

View File

@ -368,6 +368,9 @@
"payments.getStarsStatus", "payments.getStarsStatus",
"payments.getStarsTransactions", "payments.getStarsTransactions",
"payments.getStarsTransactionsByID", "payments.getStarsTransactionsByID",
"payments.getStarsSubscriptions",
"payments.changeStarsSubscription",
"payments.fulfillStarsSubscription",
"payments.sendStarsForm", "payments.sendStarsForm",
"payments.getStarsGiftOptions", "payments.getStarsGiftOptions",
"payments.getStarsGiveawayOptions", "payments.getStarsGiveawayOptions",

View File

@ -33,8 +33,12 @@
mask-image: linear-gradient(to right, transparent, black $borderStart, black $borderEnd, transparent); mask-image: linear-gradient(to right, transparent, black $borderStart, black $borderEnd, transparent);
} }
@mixin gradient-border-left($indent) { @mixin gradient-border-left($indent, $cutout: 0px) {
mask-image: linear-gradient(to right, transparent, black $indent); mask-image: linear-gradient(to right, transparent $cutout, black $indent);
}
@mixin gradient-border-right($indent, $cutout: 0px) {
mask-image: linear-gradient(to left, transparent $cutout, black $indent);
} }
@mixin gradient-border-top-bottom($top, $bottom) { @mixin gradient-border-top-bottom($top, $bottom) {

View File

@ -9,6 +9,7 @@ import type {
ApiChatInviteImporter, ApiChatInviteImporter,
ApiDocument, ApiDocument,
ApiExportedInvite, ApiExportedInvite,
ApiFakeType,
ApiLanguage, ApiLanguage,
ApiMessage, ApiMessage,
ApiPhoto, ApiPhoto,
@ -517,19 +518,25 @@ export type InlineBotSettings = {
export type CustomPeerType = 'premium' | 'toBeDistributed' | 'contacts' | 'nonContacts' export type CustomPeerType = 'premium' | 'toBeDistributed' | 'contacts' | 'nonContacts'
| 'groups' | 'channels' | 'bots' | 'excludeMuted' | 'excludeArchived' | 'excludeRead' | 'stars'; | 'groups' | 'channels' | 'bots' | 'excludeMuted' | 'excludeArchived' | 'excludeRead' | 'stars';
export interface CustomPeer { export type CustomPeer = {
isCustomPeer: true; isCustomPeer: true;
key?: string | number; key?: string | number;
titleKey: string;
subtitleKey?: string; subtitleKey?: string;
avatarIcon: IconName; avatarIcon?: IconName;
isAvatarSquare?: boolean; isAvatarSquare?: boolean;
titleValue?: number;
peerColorId?: number; peerColorId?: number;
isVerified?: boolean;
fakeType?: ApiFakeType;
customPeerAvatarColor?: string; customPeerAvatarColor?: string;
withPremiumGradient?: boolean; withPremiumGradient?: boolean;
} } & ({
titleKey: string;
title?: undefined;
} | {
title: string;
titleKey?: undefined;
});
export interface UniqueCustomPeer extends CustomPeer { export type UniqueCustomPeer<T = CustomPeerType> = CustomPeer & {
type: CustomPeerType; type: T;
} };

View File

@ -1533,9 +1533,11 @@ export interface LangPair {
'user': string | number; 'user': string | number;
'link': string | number; 'link': string | number;
}; };
'CreditsBoxOutAbout': { 'StarsTransactionTOS': {
'link': string | number; 'link': string | number;
}; };
'StarsTransactionTOSLinkText': undefined;
'StarsTransactionTOSLink': undefined;
'GiftStarsOutgoing': { 'GiftStarsOutgoing': {
'user': string | number; 'user': string | number;
}; };
@ -1549,10 +1551,23 @@ export interface LangPair {
'StarsReactionLink': undefined; 'StarsReactionLink': undefined;
'MiniAppsMoreTabs': { 'MiniAppsMoreTabs': {
'botName': string | number; 'botName': string | number;
'count': string | number;
}; };
'PrizeCredits': { 'PrizeCredits': {
'count': string | number; 'count': string | number;
}; };
'StarsSubscribeText': {
'chat': string | number;
'amount': string | number;
};
'StarsSubscribeInfo': {
'link': string | number;
};
'StarsSubscribeInfoLinkText': undefined;
'StarsSubscribeInfoLink': undefined;
'StarsPerMonth': {
'amount': string | number;
};
} }

View File

@ -59,7 +59,7 @@ export const processDeepLink = (url: string): boolean => {
const params = Object.fromEntries(searchParams); const params = Object.fromEntries(searchParams);
const { const {
openChatByInvite, checkChatInvite,
openChatByUsername, openChatByUsername,
openChatByPhoneNumber, openChatByPhoneNumber,
openStickerSet, openStickerSet,
@ -139,7 +139,7 @@ export const processDeepLink = (url: string): boolean => {
case 'join': { case 'join': {
const { invite } = params; const { invite } = params;
openChatByInvite({ hash: invite }); checkChatInvite({ hash: invite });
break; break;
} }
case 'addemoji': case 'addemoji':

View File

@ -6,6 +6,7 @@ export enum Bundles {
Main, Main,
Extra, Extra,
Calls, Calls,
Stars,
} }
interface ImportedBundles { interface ImportedBundles {
@ -13,6 +14,7 @@ interface ImportedBundles {
[Bundles.Main]: typeof import('../bundles/main'); [Bundles.Main]: typeof import('../bundles/main');
[Bundles.Extra]: typeof import('../bundles/extra'); [Bundles.Extra]: typeof import('../bundles/extra');
[Bundles.Calls]: typeof import('../bundles/calls'); [Bundles.Calls]: typeof import('../bundles/calls');
[Bundles.Stars]: typeof import('../bundles/stars');
} }
type BundlePromises = { type BundlePromises = {
@ -46,6 +48,9 @@ export async function loadBundle<B extends Bundles>(bundleName: B) {
case Bundles.Calls: case Bundles.Calls:
LOAD_PROMISES[Bundles.Calls] = import(/* webpackChunkName: "BundleCalls" */ '../bundles/calls'); LOAD_PROMISES[Bundles.Calls] = import(/* webpackChunkName: "BundleCalls" */ '../bundles/calls');
break; break;
case Bundles.Stars:
LOAD_PROMISES[Bundles.Stars] = import(/* webpackChunkName: "BundleStars" */ '../bundles/stars');
break;
} }
(LOAD_PROMISES[bundleName] as Promise<ImportedBundles[B]>).then(runCallbacks); (LOAD_PROMISES[bundleName] as Promise<ImportedBundles[B]>).then(runCallbacks);

View File

@ -10,22 +10,6 @@ export const CUSTOM_PEER_PREMIUM: UniqueCustomPeer = {
withPremiumGradient: true, withPremiumGradient: true,
}; };
export const CUSTOM_PEER_TO_BE_DISTRIBUTED: UniqueCustomPeer = {
isCustomPeer: true,
type: 'toBeDistributed',
titleKey: 'BoostingToBeDistributed',
avatarIcon: 'user',
withPremiumGradient: true,
};
export const CUSTOM_PEER_STAR: UniqueCustomPeer = {
isCustomPeer: true,
type: 'stars',
titleKey: 'Stars',
avatarIcon: 'star',
peerColorId: 1,
};
export const CUSTOM_PEER_INCLUDED_CHAT_TYPES: UniqueCustomPeer[] = [ export const CUSTOM_PEER_INCLUDED_CHAT_TYPES: UniqueCustomPeer[] = [
{ {
isCustomPeer: true, isCustomPeer: true,