Support subscription invites (#5024)
This commit is contained in:
parent
24a129c3e1
commit
28cecbfe3c
@ -8,6 +8,7 @@ import type {
|
||||
ApiChatBannedRights,
|
||||
ApiChatFolder,
|
||||
ApiChatInviteImporter,
|
||||
ApiChatInviteInfo,
|
||||
ApiChatlistExportedInvite,
|
||||
ApiChatlistInvite,
|
||||
ApiChatMember,
|
||||
@ -18,13 +19,14 @@ import type {
|
||||
ApiRestrictionReason,
|
||||
ApiSendAsPeerId,
|
||||
ApiSponsoredMessageReportResult,
|
||||
ApiStarsSubscriptionPricing,
|
||||
ApiTopic,
|
||||
} from '../../types';
|
||||
|
||||
import { omitUndefined, pick, pickTruthy } from '../../../util/iteratees';
|
||||
import { getServerTime, getServerTimeOffset } from '../../../util/serverTime';
|
||||
import { serializeBytes } from '../helpers';
|
||||
import { buildApiUsernames, buildAvatarPhotoId } from './common';
|
||||
import { addPhotoToLocalDb, addUserToLocalDb, serializeBytes } from '../helpers';
|
||||
import { buildApiPhoto, buildApiUsernames, buildAvatarPhotoId } from './common';
|
||||
import { omitVirtualClassFields } from './helpers';
|
||||
import {
|
||||
buildApiEmojiStatus,
|
||||
@ -67,6 +69,7 @@ function buildApiChatFieldsFromPeerEntity(
|
||||
? buildApiEmojiStatus(peerEntity.emojiStatus) : undefined;
|
||||
const boostLevel = ('level' in peerEntity) ? peerEntity.level : undefined;
|
||||
const areProfilesShown = Boolean('signatureProfiles' in peerEntity && peerEntity.signatureProfiles);
|
||||
const subscriptionUntil = 'subscriptionUntilDate' in peerEntity ? peerEntity.subscriptionUntilDate : undefined;
|
||||
|
||||
return omitUndefined<PeerEntityApiChatFields>({
|
||||
isMin,
|
||||
@ -100,6 +103,7 @@ function buildApiChatFieldsFromPeerEntity(
|
||||
hasStories: Boolean(maxStoryId) && !storiesUnavailable,
|
||||
emojiStatus,
|
||||
boostLevel,
|
||||
subscriptionUntil,
|
||||
});
|
||||
}
|
||||
|
||||
@ -670,3 +674,47 @@ export function buildApiSponsoredMessageReportResult(
|
||||
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(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ import type {
|
||||
ApiReceipt,
|
||||
ApiStarGiveawayOption,
|
||||
ApiStarsGiveawayWinnerOption,
|
||||
ApiStarsSubscription,
|
||||
ApiStarsTransaction,
|
||||
ApiStarsTransactionPeer,
|
||||
ApiStarTopupOption,
|
||||
@ -27,6 +28,7 @@ import type {
|
||||
} from '../../types';
|
||||
|
||||
import { addWebDocumentToLocalDb } from '../helpers';
|
||||
import { buildApiStarsSubscriptionPricing } from './chats';
|
||||
import { buildApiMessageEntity } from './common';
|
||||
import { omitVirtualClassFields } from './helpers';
|
||||
import { buildApiDocument, buildApiWebDocument, buildMessageMediaContent } from './messageContent';
|
||||
@ -504,6 +506,7 @@ export function buildApiStarsTransactionPeer(peer: GramJs.TypeStarsTransactionPe
|
||||
export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): ApiStarsTransaction {
|
||||
const {
|
||||
date, id, peer, stars, description, photo, title, refund, extendedMedia, failed, msgId, pending, gift, reaction,
|
||||
subscriptionPeriod,
|
||||
} = transaction;
|
||||
|
||||
if (photo) {
|
||||
@ -527,10 +530,28 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction):
|
||||
messageId: msgId,
|
||||
isGift: gift,
|
||||
extendedMedia: boughtExtendedMedia,
|
||||
subscriptionPeriod,
|
||||
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 {
|
||||
const {
|
||||
amount, currency, stars, extended,
|
||||
|
||||
@ -647,6 +647,12 @@ export function buildInputInvoice(invoice: ApiRequestInputInvoice) {
|
||||
});
|
||||
}
|
||||
|
||||
case 'chatInviteSubscription': {
|
||||
return new GramJs.InputInvoiceChatInviteSubscription({
|
||||
hash: invoice.hash,
|
||||
});
|
||||
}
|
||||
|
||||
case 'giveaway':
|
||||
default: {
|
||||
const purpose = buildInputStorePaymentPurpose(invoice.purpose);
|
||||
|
||||
@ -38,6 +38,7 @@ import {
|
||||
buildApiChatFromDialog,
|
||||
buildApiChatFromPreview,
|
||||
buildApiChatFromSavedDialog,
|
||||
buildApiChatInviteInfo,
|
||||
buildApiChatlistExportedInvite,
|
||||
buildApiChatlistInvite,
|
||||
buildApiChatReactions,
|
||||
@ -1404,53 +1405,27 @@ export async function migrateChat(chat: ApiChat) {
|
||||
return buildApiChatFromPreview(newChannel);
|
||||
}
|
||||
|
||||
export async function openChatByInvite(hash: string) {
|
||||
export async function checkChatInvite(hash: string) {
|
||||
const result = await invokeRequest(new GramJs.messages.CheckChatInvite({ hash }));
|
||||
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let chat: ApiChat | undefined;
|
||||
|
||||
if (result instanceof GramJs.ChatInvite) {
|
||||
const {
|
||||
photo, participantsCount, title, channel, requestNeeded, about, megagroup,
|
||||
} = result;
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
return {
|
||||
chat: undefined,
|
||||
invite: buildApiChatInviteInfo(result),
|
||||
users: result.participants?.map(buildApiUser).filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
const chat = buildApiChatFromPreview(result.chat);
|
||||
if (!chat) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { chatId: chat.id };
|
||||
return { chat, invite: undefined, users: undefined };
|
||||
}
|
||||
|
||||
export async function addChatMembers(chat: ApiChat, users: ApiUser[]) {
|
||||
|
||||
@ -20,10 +20,12 @@ import {
|
||||
buildApiReceipt,
|
||||
buildApiStarsGiftOptions,
|
||||
buildApiStarsGiveawayOptions,
|
||||
buildApiStarsSubscription,
|
||||
buildApiStarsTransaction,
|
||||
buildApiStarTopupOption,
|
||||
buildShippingOptions,
|
||||
} from '../apiBuilders/payments';
|
||||
import { buildApiPeerId } from '../apiBuilders/peers';
|
||||
import {
|
||||
buildInputInvoice, buildInputPeer, buildInputStorePaymentPurpose, buildInputThemeParams, buildShippingInfo,
|
||||
} from '../gramjsBuilders';
|
||||
@ -134,7 +136,7 @@ export async function sendStarPaymentForm({
|
||||
invoice: buildInputInvoice(inputInvoice),
|
||||
}));
|
||||
|
||||
if (!result) return false;
|
||||
if (!result) return undefined;
|
||||
|
||||
if (result instanceof GramJs.payments.PaymentVerificationNeeded) {
|
||||
if (DEBUG) {
|
||||
@ -143,11 +145,29 @@ export async function sendStarPaymentForm({
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -416,8 +436,10 @@ export async function fetchStarsStatus() {
|
||||
}
|
||||
|
||||
return {
|
||||
nextOffset: result.nextOffset,
|
||||
nextHistoryOffset: result.nextOffset,
|
||||
history: result.history?.map(buildApiStarsTransaction),
|
||||
nextSubscriptionOffset: result.subscriptionsNextOffset,
|
||||
subscriptions: result.subscriptions?.map(buildApiStarsSubscription),
|
||||
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() {
|
||||
const result = await invokeRequest(new GramJs.payments.GetStarsTopupOptions());
|
||||
|
||||
|
||||
@ -86,6 +86,8 @@ export interface ApiChat {
|
||||
hasUnreadStories?: boolean;
|
||||
maxStoryId?: number;
|
||||
|
||||
subscriptionUntil?: number;
|
||||
|
||||
// Locally determined field
|
||||
detectedLanguage?: string;
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import type { ThreadId } from '../../types';
|
||||
import type { ApiWebDocument } from './bots';
|
||||
import type { ApiGroupCall, PhoneCallAction } from './calls';
|
||||
import type { ApiChat, ApiPeerColor } from './chats';
|
||||
import type { ApiChatInviteInfo } from './misc';
|
||||
import type {
|
||||
ApiInputStorePaymentPurpose,
|
||||
ApiPremiumGiftCodeOption,
|
||||
@ -263,8 +264,15 @@ export type ApiInputInvoiceStarsGiveaway = {
|
||||
users: number;
|
||||
};
|
||||
|
||||
export type ApiInputInvoiceChatInviteSubscription = {
|
||||
type: 'chatInviteSubscription';
|
||||
hash: string;
|
||||
inviteInfo: ApiChatInviteInfo;
|
||||
};
|
||||
|
||||
export type ApiInputInvoice = ApiInputInvoiceMessage | ApiInputInvoiceSlug | ApiInputInvoiceGiveaway
|
||||
| ApiInputInvoiceGiftCode | ApiInputInvoiceStarsGift | ApiInputInvoiceStars | ApiInputInvoiceStarsGiveaway;
|
||||
| ApiInputInvoiceGiftCode | ApiInputInvoiceStarsGift | ApiInputInvoiceStars | ApiInputInvoiceStarsGiveaway
|
||||
| ApiInputInvoiceChatInviteSubscription;
|
||||
|
||||
/* Used for Invoice request */
|
||||
export type ApiRequestInputInvoiceMessage = {
|
||||
@ -294,8 +302,14 @@ export type ApiRequestInputInvoiceStarsGiveaway = {
|
||||
purpose: ApiInputStorePaymentPurpose;
|
||||
};
|
||||
|
||||
export type ApiRequestInputInvoiceChatInviteSubscription = {
|
||||
type: 'chatInviteSubscription';
|
||||
hash: string;
|
||||
};
|
||||
|
||||
export type ApiRequestInputInvoice = ApiRequestInputInvoiceMessage | ApiRequestInputInvoiceSlug
|
||||
| ApiRequestInputInvoiceGiveaway | ApiRequestInputInvoiceStars | ApiRequestInputInvoiceStarsGiveaway;
|
||||
| ApiRequestInputInvoiceGiveaway | ApiRequestInputInvoiceStars | ApiRequestInputInvoiceStarsGiveaway
|
||||
| ApiRequestInputInvoiceChatInviteSubscription;
|
||||
|
||||
export interface ApiInvoice {
|
||||
mediaType: 'invoice';
|
||||
|
||||
@ -134,16 +134,6 @@ export type ApiFieldError = {
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type ApiInviteInfo = {
|
||||
title: string;
|
||||
about?: string;
|
||||
hash: string;
|
||||
isChannel?: boolean;
|
||||
participantsCount?: number;
|
||||
isRequestNeeded?: true;
|
||||
photo?: ApiPhoto;
|
||||
};
|
||||
|
||||
export type ApiExportedInvite = {
|
||||
isRevoked?: boolean;
|
||||
isPermanent?: boolean;
|
||||
@ -159,6 +149,31 @@ export type ApiExportedInvite = {
|
||||
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 = {
|
||||
userId: string;
|
||||
date: number;
|
||||
|
||||
@ -5,6 +5,7 @@ import type { ApiChat } from './chats';
|
||||
import type {
|
||||
ApiDocument, ApiMessageEntity, ApiPaymentCredentials, BoughtPaidMedia,
|
||||
} from './messages';
|
||||
import type { ApiStarsSubscriptionPricing } from './misc';
|
||||
import type { StatisticsOverviewPercentage } from './statistics';
|
||||
import type { ApiUser } from './users';
|
||||
|
||||
@ -311,6 +312,18 @@ export interface ApiStarsTransaction {
|
||||
description?: string;
|
||||
photo?: ApiWebDocument;
|
||||
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 {
|
||||
|
||||
@ -33,7 +33,7 @@ import type {
|
||||
BoughtPaidMedia,
|
||||
} from './messages';
|
||||
import type {
|
||||
ApiEmojiInteraction, ApiError, ApiInviteInfo, ApiNotifyException, ApiSessionData,
|
||||
ApiEmojiInteraction, ApiError, ApiNotifyException, ApiSessionData,
|
||||
} from './misc';
|
||||
import type { ApiStealthMode, ApiStory, ApiStorySkipped } from './stories';
|
||||
import type {
|
||||
@ -114,11 +114,6 @@ export type ApiUpdateChatJoin = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type ApiUpdateShowInvite = {
|
||||
'@type': 'showInvite';
|
||||
data: ApiInviteInfo;
|
||||
};
|
||||
|
||||
export type ApiUpdateChatLeave = {
|
||||
'@type': 'updateChatLeave';
|
||||
id: string;
|
||||
@ -788,7 +783,7 @@ export type ApiUpdate = (
|
||||
ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateMessageTranslations |
|
||||
ApiUpdateTwoFaError | ApiUpdatePasswordError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent |
|
||||
ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy |
|
||||
ApiUpdateServerTimeOffset | ApiUpdateShowInvite | ApiUpdateMessageReactions | ApiUpdateSavedReactionTags |
|
||||
ApiUpdateServerTimeOffset | ApiUpdateMessageReactions | ApiUpdateSavedReactionTags |
|
||||
ApiUpdateGroupCallParticipants | ApiUpdateGroupCallConnection | ApiUpdateGroupCall | ApiUpdateGroupCallStreams |
|
||||
ApiUpdateGroupCallConnectionState | ApiUpdateGroupCallLeavePresentation | ApiUpdateGroupCallChatId |
|
||||
ApiUpdatePendingJoinRequests | ApiUpdatePaymentVerificationNeeded | ApiUpdatePaymentStateCompleted |
|
||||
|
||||
@ -1281,7 +1281,9 @@
|
||||
"AriaSearchOlderResult" = "Focus next result";
|
||||
"AriaSearchNewerResult" = "Focus previous result";
|
||||
"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."
|
||||
"SendPaidReaction" = "Send ⭐️{amount}"
|
||||
"StarsReactionTerms" = "By sending Stars you agree to the {link}"
|
||||
@ -1290,3 +1292,9 @@
|
||||
"MiniAppsMoreTabs_one" = "{botName} & {count} Other";
|
||||
"MiniAppsMoreTabs_other" = "{botName} & {count} Others";
|
||||
"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"
|
||||
|
||||
@ -18,19 +18,14 @@ export { default as AttachBotInstallModal } from '../components/modals/attachBot
|
||||
export { default as DeleteFolderDialog } from '../components/main/DeleteFolderDialog';
|
||||
export { default as PremiumMainModal } from '../components/main/premium/PremiumMainModal';
|
||||
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 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 StatusPickerMenu } from '../components/left/main/StatusPickerMenu';
|
||||
export { default as BoostModal } from '../components/modals/boost/BoostModal';
|
||||
export { default as GiftCodeModal } from '../components/modals/giftcode/GiftCodeModal';
|
||||
export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal';
|
||||
export { default as StarsBalanceModal } from '../components/modals/stars/StarsBalanceModal';
|
||||
export { default as StarPaymentModal } from '../components/modals/stars/StarsPaymentModal';
|
||||
export { default as StarsTransactionInfoModal } from '../components/modals/stars/transaction/StarsTransactionModal';
|
||||
export { default as PaidReactionModal } from '../components/modals/paidReaction/PaidReactionModal';
|
||||
export { default as ChatInviteModal } from '../components/modals/chatInvite/ChatInviteModal';
|
||||
|
||||
export { default as AboutAdsModal } from '../components/common/AboutAdsModal';
|
||||
export { default as AboutMonetizationModal } from '../components/common/AboutMonetizationModal';
|
||||
|
||||
7
src/bundles/stars.ts
Normal file
7
src/bundles/stars.ts
Normal 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';
|
||||
@ -220,6 +220,9 @@ const Avatar: FC<OwnProps> = ({
|
||||
} else if (chat) {
|
||||
const title = getChatTitle(lang, chat);
|
||||
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) {
|
||||
content = getFirstLetters(text, 2);
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
}
|
||||
|
||||
.fullName {
|
||||
font-size: 1rem;
|
||||
font-size: 1em;
|
||||
margin-bottom: 0;
|
||||
|
||||
&.canCopy {
|
||||
|
||||
@ -80,7 +80,7 @@ const FullNameTitle: FC<OwnProps> = ({
|
||||
|
||||
const specialTitle = useMemo(() => {
|
||||
if (customPeer) {
|
||||
return lang(customPeer.titleKey, customPeer.titleValue, 'i');
|
||||
return customPeer.title || lang(customPeer.titleKey!);
|
||||
}
|
||||
|
||||
if (isSavedMessages) {
|
||||
@ -125,8 +125,8 @@ const FullNameTitle: FC<OwnProps> = ({
|
||||
</h3>
|
||||
{!iconElement && peer && (
|
||||
<>
|
||||
{!noVerified && realPeer?.isVerified && <VerifiedIcon />}
|
||||
{!noFake && realPeer?.fakeType && <FakeIcon fakeType={realPeer.fakeType} />}
|
||||
{!noVerified && peer?.isVerified && <VerifiedIcon />}
|
||||
{!noFake && peer?.fakeType && <FakeIcon fakeType={peer.fakeType} />}
|
||||
{withEmojiStatus && realPeer?.emojiStatus && (
|
||||
<CustomEmoji
|
||||
documentId={realPeer.emojiStatus.documentId}
|
||||
|
||||
@ -14,7 +14,7 @@ import styles from './PeerBadge.module.scss';
|
||||
type OwnProps = {
|
||||
peer: ApiPeer | CustomPeer;
|
||||
text?: string;
|
||||
badgeText: string;
|
||||
badgeText?: string;
|
||||
badgeIcon?: IconName;
|
||||
className?: string;
|
||||
badgeClassName?: string;
|
||||
@ -37,10 +37,12 @@ const PeerBadge = ({
|
||||
>
|
||||
<div className={styles.top}>
|
||||
<Avatar size="large" peer={peer} />
|
||||
<div className={buildClassName(styles.badge, badgeClassName)}>
|
||||
{badgeIcon && <Icon name={badgeIcon} />}
|
||||
{badgeText}
|
||||
</div>
|
||||
{badgeText && (
|
||||
<div className={buildClassName(styles.badge, badgeClassName)}>
|
||||
{badgeIcon && <Icon name={badgeIcon} />}
|
||||
{badgeText}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{text && <p className={styles.text}>{text}</p>}
|
||||
</div>
|
||||
|
||||
@ -10,7 +10,7 @@ export function getPeerColorClass(peer?: ApiPeer | CustomPeer, noUserColors?: bo
|
||||
}
|
||||
|
||||
if ('isCustomPeer' in peer) {
|
||||
if (!peer.peerColorId) return undefined;
|
||||
if (peer.peerColorId === undefined) return undefined;
|
||||
return `peer-color-${peer.peerColorId}`;
|
||||
}
|
||||
return noUserColors ? `peer-color-count-${getPeerColorCount(peer)}` : `peer-color-${getPeerColorKey(peer)}`;
|
||||
|
||||
@ -80,7 +80,7 @@ const PickerSelectedItem = <T,>({
|
||||
/>
|
||||
);
|
||||
|
||||
const name = (customPeer && lang(customPeer.titleKey))
|
||||
const name = (customPeer && (customPeer.title || lang(customPeer.titleKey!)))
|
||||
|| (!chat || (user && !isSavedMessages)
|
||||
? getUserFirstOrLastName(user)
|
||||
: getChatTitle(lang, chat, isSavedMessages));
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
@use "../../../styles/mixins";
|
||||
|
||||
.Chat {
|
||||
--background-color: var(--color-background);
|
||||
--thumbs-background: var(--background-color);
|
||||
@ -39,8 +41,8 @@
|
||||
|
||||
&:hover,
|
||||
&.ListItem.has-menu-open {
|
||||
.avatar-online {
|
||||
border-color: var(--color-chat-hover);
|
||||
.avatar-badge {
|
||||
--_color-outline: var(--color-chat-hover);
|
||||
}
|
||||
|
||||
.avatar-badge-wrapper {
|
||||
@ -70,8 +72,8 @@
|
||||
&.selected {
|
||||
--background-color: var(--color-chat-hover) !important;
|
||||
|
||||
.avatar-online {
|
||||
border-color: var(--color-chat-hover);
|
||||
.avatar-badge {
|
||||
--_color-outline: var(--color-chat-hover);
|
||||
}
|
||||
|
||||
.ChatCallStatus {
|
||||
@ -94,8 +96,11 @@
|
||||
--color-checkmark: var(--color-primary);
|
||||
}
|
||||
|
||||
.avatar-badge {
|
||||
--_color-outline: var(--color-chat-active) !important;
|
||||
}
|
||||
|
||||
.avatar-online {
|
||||
border-color: var(--color-chat-active) !important;
|
||||
background-color: var(--color-white);
|
||||
}
|
||||
|
||||
@ -241,16 +246,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-online {
|
||||
.avatar-badge {
|
||||
--_color-outline: var(--color-background);
|
||||
position: absolute;
|
||||
bottom: 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;
|
||||
height: 0.875rem;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--color-background);
|
||||
background-color: #0ac630;
|
||||
flex-shrink: 0;
|
||||
|
||||
opacity: 0.5;
|
||||
transform: scale(0);
|
||||
|
||||
@ -64,6 +64,7 @@ import useChatListEntry from './hooks/useChatListEntry';
|
||||
import Avatar from '../../common/Avatar';
|
||||
import DeleteChatModal from '../../common/DeleteChatModal';
|
||||
import FullNameTitle from '../../common/FullNameTitle';
|
||||
import StarIcon from '../../common/icons/StarIcon';
|
||||
import LastMessageMeta from '../../common/LastMessageMeta';
|
||||
import ReportModal from '../../common/ReportModal';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
@ -345,12 +346,17 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
isSavedDialog={isSavedDialog}
|
||||
size={isPreview ? 'medium' : 'large'}
|
||||
withStory={!user?.isSelf}
|
||||
withStoryGap={isAvatarOnlineShown}
|
||||
withStoryGap={isAvatarOnlineShown || Boolean(chat.subscriptionUntil)}
|
||||
storyViewerOrigin={StoryViewerOrigin.ChatList}
|
||||
storyViewerMode="single-peer"
|
||||
/>
|
||||
<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
|
||||
chat={chat}
|
||||
isMuted={isMuted}
|
||||
|
||||
@ -3,7 +3,7 @@ import React, { memo, useEffect } from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type {
|
||||
ApiContact, ApiError, ApiInviteInfo, ApiPhoto,
|
||||
ApiContact, ApiError,
|
||||
} from '../../api/types';
|
||||
import type { MessageList } from '../../global/types';
|
||||
|
||||
@ -15,21 +15,18 @@ import renderText from '../common/helpers/renderText';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import Avatar from '../common/Avatar';
|
||||
import Button from '../ui/Button';
|
||||
import Modal from '../ui/Modal';
|
||||
|
||||
type StateProps = {
|
||||
currentMessageList?: MessageList;
|
||||
dialogs: (ApiError | ApiInviteInfo | ApiContact)[];
|
||||
dialogs: (ApiError | ApiContact)[];
|
||||
};
|
||||
|
||||
const Dialogs: FC<StateProps> = ({ dialogs, currentMessageList }) => {
|
||||
const {
|
||||
dismissDialog,
|
||||
acceptInviteConfirmation,
|
||||
sendMessage,
|
||||
showNotification,
|
||||
} = getActions();
|
||||
const [isModalOpen, openModal, closeModal] = useFlag();
|
||||
|
||||
@ -45,77 +42,6 @@ const Dialogs: FC<StateProps> = ({ dialogs, currentMessageList }) => {
|
||||
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 handleConfirm = () => {
|
||||
if (!currentMessageList) {
|
||||
@ -171,11 +97,7 @@ const Dialogs: FC<StateProps> = ({ dialogs, currentMessageList }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const renderDialog = (dialog: ApiError | ApiInviteInfo | ApiContact) => {
|
||||
if ('hash' in dialog) {
|
||||
return renderInvite(dialog);
|
||||
}
|
||||
|
||||
const renderDialog = (dialog: ApiError | ApiContact) => {
|
||||
if ('phoneNumber' in dialog) {
|
||||
return renderContactRequest(dialog);
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||
|
||||
const StarsGiftModalAsync: FC<OwnProps> = (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
|
||||
return StarsGiftModal ? <StarsGiftModal {...props} /> : undefined;
|
||||
|
||||
@ -9,7 +9,7 @@ import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||
|
||||
const StarsGiftingPickerModalAsync: FC<OwnProps> = (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
|
||||
return StarsGiftingPickerModal ? <StarsGiftingPickerModal {...props} /> : undefined;
|
||||
|
||||
@ -8,6 +8,7 @@ import { pick } from '../../util/iteratees';
|
||||
|
||||
import AttachBotInstallModal from './attachBotInstall/AttachBotInstallModal.async';
|
||||
import BoostModal from './boost/BoostModal.async';
|
||||
import ChatInviteModal from './chatInvite/ChatInviteModal.async';
|
||||
import ChatlistModal from './chatlist/ChatlistModal.async';
|
||||
import CollectibleInfoModal from './collectible/CollectibleInfoModal.async';
|
||||
import GiftCodeModal from './giftcode/GiftCodeModal.async';
|
||||
@ -18,6 +19,7 @@ import PaidReactionModal from './paidReaction/PaidReactionModal.async';
|
||||
import ReportAdModal from './reportAd/ReportAdModal.async';
|
||||
import StarsBalanceModal from './stars/StarsBalanceModal.async';
|
||||
import StarsPaymentModal from './stars/StarsPaymentModal.async';
|
||||
import StarsSubscriptionModal from './stars/subscription/StarsSubscriptionModal.async';
|
||||
import StarsTransactionInfoModal from './stars/transaction/StarsTransactionModal.async';
|
||||
import UrlAuthModal from './urlAuth/UrlAuthModal.async';
|
||||
import WebAppModal from './webApp/WebAppModal.async';
|
||||
@ -39,7 +41,9 @@ type ModalKey = keyof Pick<TabState,
|
||||
'starsTransactionModal' |
|
||||
'paidReactionModal' |
|
||||
'webApps' |
|
||||
'starsTransactionModal'
|
||||
'starsTransactionModal' |
|
||||
'chatInviteModal' |
|
||||
'starsSubscriptionModal'
|
||||
>;
|
||||
|
||||
type StateProps = {
|
||||
@ -69,7 +73,9 @@ const MODALS: ModalRegistry = {
|
||||
isStarPaymentModalOpen: StarsPaymentModal,
|
||||
starsBalanceModal: StarsBalanceModal,
|
||||
starsTransactionModal: StarsTransactionInfoModal,
|
||||
chatInviteModal: ChatInviteModal,
|
||||
paidReactionModal: PaidReactionModal,
|
||||
starsSubscriptionModal: StarsSubscriptionModal,
|
||||
};
|
||||
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
|
||||
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;
|
||||
|
||||
18
src/components/modals/chatInvite/ChatInviteModal.async.tsx
Normal file
18
src/components/modals/chatInvite/ChatInviteModal.async.tsx
Normal 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;
|
||||
31
src/components/modals/chatInvite/ChatInviteModal.module.scss
Normal file
31
src/components/modals/chatInvite/ChatInviteModal.module.scss
Normal 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;
|
||||
}
|
||||
108
src/components/modals/chatInvite/ChatInviteModal.tsx
Normal file
108
src/components/modals/chatInvite/ChatInviteModal.tsx
Normal 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);
|
||||
@ -13,56 +13,28 @@
|
||||
}
|
||||
|
||||
.value {
|
||||
background-color: var(--color-background);
|
||||
word-break: break-word;
|
||||
min-width: 2rem;
|
||||
}
|
||||
|
||||
.table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
display: grid;
|
||||
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 {
|
||||
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;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
padding: 0.5rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
position: relative;
|
||||
|
||||
@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;
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React, { memo, type TeactNode } from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { ApiPeer, ApiWebDocument } from '../../../api/types';
|
||||
import type { ApiPeer } from '../../../api/types';
|
||||
import type { CustomPeer } from '../../../types';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
@ -15,8 +15,6 @@ import Modal from '../../ui/Modal';
|
||||
|
||||
import styles from './TableInfoModal.module.scss';
|
||||
|
||||
import StarsBackground from '../../../assets/stars-bg.png';
|
||||
|
||||
type ChatItem = { chatId: string };
|
||||
|
||||
export type TableData = [TeactNode, TeactNode | ChatItem][];
|
||||
@ -25,13 +23,7 @@ type OwnProps = {
|
||||
isOpen?: boolean;
|
||||
title?: string;
|
||||
tableData?: TableData;
|
||||
headerImageUrl?: string;
|
||||
logoBackground?: string;
|
||||
headerAvatarPeer?: ApiPeer | CustomPeer;
|
||||
headerAvatarWebPhoto?: ApiWebDocument;
|
||||
noHeaderImage?: boolean;
|
||||
isGift?: boolean;
|
||||
isPrizeStars?: boolean;
|
||||
header?: TeactNode;
|
||||
footer?: TeactNode;
|
||||
buttonText?: string;
|
||||
@ -44,13 +36,7 @@ const TableInfoModal = ({
|
||||
isOpen,
|
||||
title,
|
||||
tableData,
|
||||
headerImageUrl,
|
||||
logoBackground,
|
||||
headerAvatarPeer,
|
||||
headerAvatarWebPhoto,
|
||||
noHeaderImage,
|
||||
isGift,
|
||||
isPrizeStars,
|
||||
header,
|
||||
footer,
|
||||
buttonText,
|
||||
@ -64,8 +50,6 @@ const TableInfoModal = ({
|
||||
onClose();
|
||||
});
|
||||
|
||||
const withAvatar = Boolean(headerAvatarPeer || headerAvatarWebPhoto);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
@ -77,23 +61,15 @@ const TableInfoModal = ({
|
||||
contentClassName={styles.content}
|
||||
onClose={onClose}
|
||||
>
|
||||
{!isGift && !isPrizeStars && !noHeaderImage && (
|
||||
withAvatar ? (
|
||||
<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>
|
||||
)
|
||||
{headerAvatarPeer && (
|
||||
<Avatar peer={headerAvatarPeer} size="jumbo" className={styles.avatar} />
|
||||
)}
|
||||
{header}
|
||||
<table className={styles.table}>
|
||||
<div className={styles.table}>
|
||||
{tableData?.map(([label, value]) => (
|
||||
<tr className={styles.row}>
|
||||
<td className={buildClassName(styles.cell, styles.title)}>{label}</td>
|
||||
<td className={buildClassName(styles.cell, styles.value)}>
|
||||
<>
|
||||
<div className={buildClassName(styles.cell, styles.title)}>{label}</div>
|
||||
<div className={buildClassName(styles.cell, styles.value)}>
|
||||
{typeof value === 'object' && 'chatId' in value ? (
|
||||
<PickerSelectedItem
|
||||
peerId={value.chatId}
|
||||
@ -104,10 +80,10 @@ const TableInfoModal = ({
|
||||
onClick={handleOpenChat}
|
||||
/>
|
||||
) : value}
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</table>
|
||||
</div>
|
||||
{footer}
|
||||
{buttonText && (
|
||||
<Button size="smaller" onClick={onButtonClick || onClose}>{buttonText}</Button>
|
||||
|
||||
@ -6,3 +6,10 @@
|
||||
.centered {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 7.5rem;
|
||||
height: 7.5rem;
|
||||
margin-bottom: 1rem;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@ -69,6 +69,7 @@ const GiftCodeModal = ({
|
||||
|
||||
const header = (
|
||||
<>
|
||||
<img src={PremiumLogo} alt="" className={styles.logo} />
|
||||
<p className={styles.centered}>{renderText(lang('lng_gift_link_about'), ['simple_markdown'])}</p>
|
||||
<LinkField title="BoostingGiftLink" link={`${TME_LINK_PREFIX}/${GIFTCODE_PATH}/${slug}`} />
|
||||
</>
|
||||
@ -110,7 +111,6 @@ const GiftCodeModal = ({
|
||||
<TableInfoModal
|
||||
isOpen={isOpen}
|
||||
title={lang('lng_gift_link_title')}
|
||||
headerImageUrl={PremiumLogo}
|
||||
tableData={modalData.tableData}
|
||||
header={modalData.header}
|
||||
footer={modalData.footer}
|
||||
|
||||
@ -9,7 +9,7 @@ import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||
|
||||
const PaidReactionModalAsync: FC<OwnProps> = (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
|
||||
return PaidReactionModal ? <PaidReactionModal {...props} /> : undefined;
|
||||
|
||||
@ -9,7 +9,7 @@ import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||
|
||||
const StarsBalanceModalAsync: FC<OwnProps> = (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
|
||||
return StarsBalanceModal ? <StarsBalanceModal {...props} /> : undefined;
|
||||
|
||||
@ -42,6 +42,14 @@
|
||||
@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 {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
@ -76,6 +84,7 @@
|
||||
margin-inline: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
text-wrap: balance;
|
||||
line-height: 1.375;
|
||||
}
|
||||
|
||||
.header {
|
||||
@ -123,10 +132,11 @@
|
||||
}
|
||||
|
||||
.balanceBottom {
|
||||
line-height: 1.5;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.modalBalance {
|
||||
@ -162,6 +172,17 @@
|
||||
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 {
|
||||
height: 7rem;
|
||||
position: absolute;
|
||||
@ -179,13 +200,27 @@
|
||||
.paymentButton {
|
||||
display: flex;
|
||||
gap: 0.125rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.paymentButtonStar {
|
||||
--color-fill: white !important;
|
||||
}
|
||||
|
||||
.transactions {
|
||||
.transactions, .subscriptions {
|
||||
display: flex;
|
||||
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);
|
||||
}
|
||||
|
||||
@ -26,7 +26,8 @@ import TabList, { type TabWithProperties } from '../../ui/TabList';
|
||||
import Transition from '../../ui/Transition';
|
||||
import BalanceBlock from './BalanceBlock';
|
||||
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';
|
||||
|
||||
@ -39,6 +40,7 @@ const TRANSACTION_TABS: TabWithProperties[] = [
|
||||
{ title: 'StarsTransactionsIncoming' },
|
||||
{ title: 'StarsTransactionsOutgoing' },
|
||||
];
|
||||
const TRANSACTION_ITEM_CLASS = 'StarsTransactionItem';
|
||||
|
||||
export type OwnProps = {
|
||||
modal: TabState['starsBalanceModal'];
|
||||
@ -56,7 +58,7 @@ const StarsBalanceModal = ({
|
||||
closeStarsBalanceModal, loadStarsTransactions, openStarsGiftingModal, openInvoice,
|
||||
} = getActions();
|
||||
|
||||
const { balance, history } = starsBalanceState || {};
|
||||
const { balance, history, subscriptions } = starsBalanceState || {};
|
||||
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
@ -90,13 +92,14 @@ const StarsBalanceModal = ({
|
||||
return undefined;
|
||||
}, [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;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setHeaderHidden(true);
|
||||
setSelectedTabIndex(0);
|
||||
hideBuyOptions();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
@ -127,7 +130,7 @@ const StarsBalanceModal = ({
|
||||
setHeaderHidden(scrollTop <= 150);
|
||||
}
|
||||
|
||||
const handleLoadMore = useLastCallback(() => {
|
||||
const handleLoadMoreTransactions = useLastCallback(() => {
|
||||
loadStarsTransactions({
|
||||
type: TRANSACTION_TYPES[selectedTabIndex],
|
||||
});
|
||||
@ -170,7 +173,7 @@ const StarsBalanceModal = ({
|
||||
<img className={styles.logo} src={StarLogo} alt="" draggable={false} />
|
||||
<img className={styles.logoBackground} src={StarsBackground} alt="" draggable={false} />
|
||||
<h2 className={styles.headerText}>
|
||||
{starsNeeded ? oldLang('StarsNeededTitle', starsNeeded) : oldLang('TelegramStars')}
|
||||
{starsNeeded ? oldLang('StarsNeededTitle', ongoingTransactionAmount) : oldLang('TelegramStars')}
|
||||
</h2>
|
||||
<div className={styles.description}>
|
||||
{renderText(
|
||||
@ -207,7 +210,20 @@ const StarsBalanceModal = ({
|
||||
<div className={styles.secondaryInfo}>
|
||||
{tosText}
|
||||
</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.section}>
|
||||
<Transition
|
||||
@ -218,21 +234,25 @@ const StarsBalanceModal = ({
|
||||
className={styles.transition}
|
||||
>
|
||||
<InfiniteScroll
|
||||
onLoadMore={handleLoadMore}
|
||||
onLoadMore={handleLoadMoreTransactions}
|
||||
items={history?.[TRANSACTION_TYPES[selectedTabIndex]]?.transactions}
|
||||
scrollContainerClosest={`.${styles.main}`}
|
||||
itemSelector={`.${TRANSACTION_ITEM_CLASS}`}
|
||||
className={styles.transactions}
|
||||
noFastList
|
||||
>
|
||||
{history?.[TRANSACTION_TYPES[selectedTabIndex]]?.transactions.map((transaction) => (
|
||||
<TransactionItem
|
||||
<StarsTransactionItem
|
||||
key={`${transaction.id}-${transaction.isRefund}`}
|
||||
transaction={transaction}
|
||||
className={TRANSACTION_ITEM_CLASS}
|
||||
/>
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
</Transition>
|
||||
</div>
|
||||
<TabList
|
||||
className={styles.tabs}
|
||||
activeTab={selectedTabIndex}
|
||||
tabs={TRANSACTION_TABS}
|
||||
onSwitchTab={setSelectedTabIndex}
|
||||
|
||||
@ -9,7 +9,7 @@ import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||
|
||||
const StarPaymentModalAsync: FC<OwnProps> = (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
|
||||
return StarPaymentModal ? <StarPaymentModal {...props} /> : undefined;
|
||||
|
||||
@ -2,11 +2,11 @@ import React, { memo, useEffect, useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiMediaExtendedPreview, ApiMessage, ApiUser,
|
||||
ApiChat, ApiChatInviteInfo, ApiMediaExtendedPreview, ApiMessage, ApiUser,
|
||||
} from '../../../api/types';
|
||||
import type { GlobalState, TabState } from '../../../global/types';
|
||||
|
||||
import { getChatTitle, getUserFullName } from '../../../global/helpers';
|
||||
import { getChatTitle, getCustomPeerFromInvite, getUserFullName } from '../../../global/helpers';
|
||||
import {
|
||||
selectChat, selectChatMessage, selectTabState, selectUser,
|
||||
} from '../../../global/selectors';
|
||||
@ -14,11 +14,13 @@ import buildClassName from '../../../util/buildClassName';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Avatar from '../../common/Avatar';
|
||||
import StarIcon from '../../common/icons/StarIcon';
|
||||
import SafeLink from '../../common/SafeLink';
|
||||
import Button from '../../ui/Button';
|
||||
import Modal from '../../ui/Modal';
|
||||
import BalanceBlock from './BalanceBlock';
|
||||
@ -38,6 +40,7 @@ type StateProps = {
|
||||
bot?: ApiUser;
|
||||
paidMediaMessage?: ApiMessage;
|
||||
paidMediaChat?: ApiChat;
|
||||
inviteInfo?: ApiChatInviteInfo;
|
||||
};
|
||||
|
||||
const StarPaymentModal = ({
|
||||
@ -47,6 +50,7 @@ const StarPaymentModal = ({
|
||||
payment,
|
||||
paidMediaMessage,
|
||||
paidMediaChat,
|
||||
inviteInfo,
|
||||
}: OwnProps & StateProps) => {
|
||||
const { closePaymentModal, openStarsBalanceModal, sendStarPaymentForm } = getActions();
|
||||
const [isLoading, markLoading, unmarkLoading] = useFlag();
|
||||
@ -54,7 +58,8 @@ const StarPaymentModal = ({
|
||||
|
||||
const photo = payment?.invoice?.photo;
|
||||
|
||||
const lang = useOldLang();
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
@ -68,23 +73,54 @@ const StarPaymentModal = ({
|
||||
}
|
||||
|
||||
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) {
|
||||
const extendedMedia = paidMediaMessage.content.paidMedia!.extendedMedia as ApiMediaExtendedPreview[];
|
||||
const areAllPhotos = extendedMedia.every((media) => !media.duration);
|
||||
const areAllVideos = extendedMedia.every((media) => !!media.duration);
|
||||
|
||||
const mediaText = areAllPhotos ? lang('Stars.Transfer.Photos', extendedMedia.length)
|
||||
: areAllVideos ? lang('Stars.Transfer.Videos', extendedMedia.length)
|
||||
: lang('Media', extendedMedia.length);
|
||||
const mediaText = areAllPhotos ? oldLang('Stars.Transfer.Photos', extendedMedia.length)
|
||||
: areAllVideos ? oldLang('Stars.Transfer.Videos', extendedMedia.length)
|
||||
: oldLang('Media', extendedMedia.length);
|
||||
|
||||
const channelTitle = getChatTitle(lang, paidMediaChat!);
|
||||
return lang('Stars.Transfer.UnlockInfo', [mediaText, channelTitle, starsText]);
|
||||
const channelTitle = getChatTitle(oldLang, paidMediaChat!);
|
||||
return oldLang('Stars.Transfer.UnlockInfo', [mediaText, channelTitle, starsText]);
|
||||
}
|
||||
|
||||
return lang('Stars.Transfer.Info', [payment.invoice.title, botName, starsText]);
|
||||
}, [payment?.invoice, bot, lang, paidMediaMessage, paidMediaChat]);
|
||||
if (inviteInfo) {
|
||||
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 price = payment?.invoice?.amount;
|
||||
@ -113,9 +149,14 @@ const StarPaymentModal = ({
|
||||
onClose={closePaymentModal}
|
||||
>
|
||||
<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 ? (
|
||||
<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" />
|
||||
@ -125,18 +166,23 @@ const StarPaymentModal = ({
|
||||
<img className={styles.paymentImageBackground} src={StarsBackground} alt="" draggable={false} />
|
||||
</div>
|
||||
<h2 className={styles.headerText}>
|
||||
{lang('StarsConfirmPurchaseTitle')}
|
||||
{inviteCustomPeer ? oldLang('StarsSubscribeTitle') : oldLang('StarsConfirmPurchaseTitle')}
|
||||
</h2>
|
||||
<div className={buildClassName(styles.description, styles.smallerText)}>
|
||||
<div className={styles.description}>
|
||||
{renderText(descriptionText, ['simple_markdown', 'emoji'])}
|
||||
</div>
|
||||
<Button className={styles.paymentButton} size="smaller" onClick={handlePayment} isLoading={isLoading}>
|
||||
{lang('Stars.Transfer.Pay')}
|
||||
{oldLang('Stars.Transfer.Pay')}
|
||||
<div className={styles.paymentAmount}>
|
||||
{payment?.invoice?.amount}
|
||||
<StarIcon className={styles.paymentButtonStar} size="small" />
|
||||
</div>
|
||||
</Button>
|
||||
{disclaimerText && (
|
||||
<div className={buildClassName(styles.disclaimer, styles.smallerText)}>
|
||||
{disclaimerText}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@ -152,12 +198,17 @@ export default memo(withGlobal<OwnProps>(
|
||||
const chat = messageInputInvoice ? selectChat(global, messageInputInvoice.chatId) : undefined;
|
||||
const isPaidMedia = message?.content.paidMedia;
|
||||
|
||||
const inviteInputInvoice = payment.inputInvoice?.type === 'chatInviteSubscription'
|
||||
? payment.inputInvoice : undefined;
|
||||
const inviteInfo = inviteInputInvoice?.inviteInfo;
|
||||
|
||||
return {
|
||||
bot,
|
||||
starsBalanceState: global.stars,
|
||||
payment,
|
||||
paidMediaMessage: isPaidMedia ? message : undefined,
|
||||
paidMediaChat: isPaidMedia ? chat : undefined,
|
||||
inviteInfo,
|
||||
};
|
||||
},
|
||||
)(StarPaymentModal));
|
||||
|
||||
@ -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));
|
||||
}
|
||||
@ -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);
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
@ -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));
|
||||
@ -1,7 +1,9 @@
|
||||
@use '../../../../styles/mixins';
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
padding: 0.25rem;
|
||||
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;
|
||||
@ -28,10 +30,6 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.star {
|
||||
margin-top: -0.25rem;
|
||||
}
|
||||
|
||||
.title, .description, .date {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@ -56,3 +54,17 @@
|
||||
.negative {
|
||||
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));
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ import styles from './StarsTransactionItem.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
transaction: ApiStarsTransaction;
|
||||
className?: 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 {
|
||||
date,
|
||||
@ -43,6 +44,7 @@ const StarsTransactionItem = ({ transaction }: OwnProps) => {
|
||||
photo,
|
||||
peer: transactionPeer,
|
||||
extendedMedia,
|
||||
subscriptionPeriod,
|
||||
} = transaction;
|
||||
const lang = useOldLang();
|
||||
|
||||
@ -50,15 +52,13 @@ const StarsTransactionItem = ({ transaction }: OwnProps) => {
|
||||
const peer = useSelector(selectOptionalPeer(peerId));
|
||||
|
||||
const data = useMemo(() => {
|
||||
let title = transaction.title;
|
||||
if (transaction.extendedMedia) {
|
||||
title = lang('StarMediaPurchase');
|
||||
}
|
||||
|
||||
if (transaction.isReaction) {
|
||||
title = lang('StarsReactionsSent');
|
||||
}
|
||||
let title = (() => {
|
||||
if (transaction.extendedMedia) return lang('StarMediaPurchase');
|
||||
if (transaction.subscriptionPeriod) return lang('StarSubscriptionPurchase');
|
||||
if (transaction.isReaction) return lang('StarsReactionsSent');
|
||||
|
||||
return transaction.title;
|
||||
})();
|
||||
let description;
|
||||
let status: string | undefined;
|
||||
let avatarPeer: ApiPeer | CustomPeer | undefined;
|
||||
@ -68,7 +68,7 @@ const StarsTransactionItem = ({ transaction }: OwnProps) => {
|
||||
avatarPeer = peer || CUSTOM_PEER_PREMIUM;
|
||||
} else {
|
||||
const customPeer = buildStarsTransactionCustomPeer(transaction.peer);
|
||||
title = lang(customPeer.titleKey);
|
||||
title = customPeer.title || lang(customPeer.titleKey!);
|
||||
description = lang(customPeer.subtitleKey!);
|
||||
avatarPeer = customPeer;
|
||||
}
|
||||
@ -102,9 +102,14 @@ const StarsTransactionItem = ({ transaction }: OwnProps) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.root} onClick={handleClick}>
|
||||
{extendedMedia ? <PaidMediaThumb media={extendedMedia} isTransactionPreview />
|
||||
: <Avatar size="medium" webPhoto={photo} peer={data.avatarPeer} />}
|
||||
<div className={buildClassName(styles.root, className)} onClick={handleClick}>
|
||||
<div className={styles.preview}>
|
||||
{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}>
|
||||
<h3 className={styles.title}>{data.title}</h3>
|
||||
<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)}>
|
||||
{formatStarsTransactionAmount(stars)}
|
||||
</span>
|
||||
<StarIcon className={styles.star} type="gold" size="big" />
|
||||
<StarIcon className={styles.star} type="gold" size="adaptive" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -9,7 +9,7 @@ import useModuleLoader from '../../../../hooks/useModuleLoader';
|
||||
|
||||
const StarsTransactionModalAsync: FC<OwnProps> = (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
|
||||
return StarsTransactionModal ? <StarsTransactionModal {...props} /> : undefined;
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
@use "../../../../styles/mixins";
|
||||
|
||||
.modal {
|
||||
z-index: calc(var(--z-modal-low-priority) + 1);
|
||||
}
|
||||
|
||||
.modal :global(.modal-dialog) {
|
||||
max-width: 26rem !important;
|
||||
}
|
||||
|
||||
.positive {
|
||||
color: var(--color-success);
|
||||
}
|
||||
@ -43,6 +41,11 @@
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
@include mixins.gradient-border-right(3rem, 1rem);
|
||||
}
|
||||
|
||||
.description {
|
||||
@ -53,27 +56,31 @@
|
||||
text-align: center;
|
||||
margin-block: 0.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.starsBackground {
|
||||
position: absolute;
|
||||
height: 8rem;
|
||||
top: -8.5rem;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.mediaShift {
|
||||
top: -1.5rem;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.copyIcon {
|
||||
position: absolute;
|
||||
right: 0.25rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
||||
margin-inline-start: 0.25rem;
|
||||
color: var(--color-primary);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mediaPreview {
|
||||
margin-bottom: 2rem;
|
||||
margin-block: 1.5rem 1rem;
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
}
|
||||
|
||||
|
||||
@ -24,9 +24,10 @@ import renderText from '../../../common/helpers/renderText';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../../hooks/useOldLang';
|
||||
import usePreviousDeprecated from '../../../../hooks/usePreviousDeprecated';
|
||||
import usePrevious from '../../../../hooks/usePrevious';
|
||||
|
||||
import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker';
|
||||
import Avatar from '../../../common/Avatar';
|
||||
import Icon from '../../../common/icons/Icon';
|
||||
import StarIcon from '../../../common/icons/StarIcon';
|
||||
import SafeLink from '../../../common/SafeLink';
|
||||
@ -55,8 +56,6 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
const { transaction } = modal || {};
|
||||
const isGift = transaction?.isGift;
|
||||
const isPrizeStars = transaction?.isPrizeStars;
|
||||
|
||||
const handleOpenMedia = useLastCallback(() => {
|
||||
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 subtitleText = oldLang('lng_credits_box_history_entry_gift_in_about');
|
||||
const subtitleTextParts = subtitleText.split('{link}');
|
||||
@ -127,24 +110,23 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { isGift, isPrizeStars, photo } = transaction;
|
||||
|
||||
const customPeer = (transaction.peer && transaction.peer.type !== 'peer'
|
||||
&& buildStarsTransactionCustomPeer(transaction.peer)) || undefined;
|
||||
|
||||
const peerId = transaction.peer?.type === 'peer' ? transaction.peer.id : undefined;
|
||||
const toName = transaction.peer && oldLang(getStarsPeerTitleKey(transaction.peer));
|
||||
|
||||
let title = transaction.title;
|
||||
if (!title && customPeer) {
|
||||
title = oldLang(customPeer.titleKey);
|
||||
}
|
||||
const title = (() => {
|
||||
if (transaction.extendedMedia) return oldLang('StarMediaPurchase');
|
||||
if (transaction.subscriptionPeriod) return oldLang('StarSubscriptionPurchase');
|
||||
if (transaction.isReaction) return oldLang('StarsReactionsSent');
|
||||
|
||||
if (!title && transaction.extendedMedia) {
|
||||
title = oldLang('StarMediaPurchase');
|
||||
}
|
||||
if (customPeer) return customPeer.title || oldLang(customPeer.titleKey!);
|
||||
|
||||
if (!title && transaction.isReaction) {
|
||||
title = oldLang('StarsReactionsSent');
|
||||
}
|
||||
return transaction.title;
|
||||
})();
|
||||
|
||||
const messageLink = peer && transaction.messageId
|
||||
? getMessageLink(peer, undefined, transaction.messageId) : undefined;
|
||||
@ -161,6 +143,9 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const description = transaction.description || (media ? mediaText : undefined);
|
||||
|
||||
const shouldDisplayAvatar = !media && !isGift && !isPrizeStars;
|
||||
const avatarPeer = !photo ? (peer || customPeer) : undefined;
|
||||
|
||||
const header = (
|
||||
<div className={buildClassName(styles.header, styles.starsHeader)}>
|
||||
{media && (
|
||||
@ -170,14 +155,24 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
onClick={handleOpenMedia}
|
||||
/>
|
||||
)}
|
||||
{(isGift || isPrizeStars) ? animatedStickerData : (
|
||||
<img
|
||||
className={buildClassName(styles.starsBackground, media && styles.mediaShift)}
|
||||
src={StarsBackground}
|
||||
alt=""
|
||||
draggable={false}
|
||||
{(isGift || isPrizeStars) && starGiftSticker && (
|
||||
<AnimatedIconFromSticker
|
||||
key={transaction.id}
|
||||
sticker={starGiftSticker}
|
||||
play={canPlayAnimatedEmojis}
|
||||
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>}
|
||||
{(isGift || isPrizeStars) && (
|
||||
<h1 className={buildClassName(styles.title, styles.starTitle)}>
|
||||
@ -223,18 +218,20 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
tableData.push([
|
||||
oldLang('Stars.Transaction.Id'),
|
||||
(
|
||||
<span
|
||||
className={styles.tid}
|
||||
onClick={() => {
|
||||
copyTextToClipboard(transaction.id!);
|
||||
showNotification({
|
||||
message: oldLang('StarsTransactionIDCopied'),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{transaction.id}
|
||||
<>
|
||||
<div
|
||||
className={styles.tid}
|
||||
onClick={() => {
|
||||
copyTextToClipboard(transaction.id!);
|
||||
showNotification({
|
||||
message: oldLang('StarsTransactionIDCopied'),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{transaction.id}
|
||||
</div>
|
||||
<Icon className={styles.copyIcon} name="copy" />
|
||||
</span>
|
||||
</>
|
||||
),
|
||||
]);
|
||||
}
|
||||
@ -259,13 +256,12 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
header,
|
||||
tableData,
|
||||
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;
|
||||
|
||||
return (
|
||||
@ -273,13 +269,8 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
isOpen={Boolean(transaction)}
|
||||
className={styles.modal}
|
||||
header={renderingModalData?.header}
|
||||
isGift={isGift}
|
||||
isPrizeStars={isPrizeStars}
|
||||
tableData={renderingModalData?.tableData}
|
||||
footer={renderingModalData?.footer}
|
||||
noHeaderImage={Boolean(transaction?.extendedMedia)}
|
||||
headerAvatarWebPhoto={transaction?.photo}
|
||||
headerAvatarPeer={renderingModalData?.avatarPeer}
|
||||
buttonText={oldLang('OK')}
|
||||
onClose={closeStarsTransactionModal}
|
||||
/>
|
||||
|
||||
@ -5,6 +5,7 @@ import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiBoost, ApiBoostStatistics, ApiTypePrepaidGiveaway } from '../../../api/types';
|
||||
import type { TabState } from '../../../global/types';
|
||||
import type { CustomPeer } from '../../../types';
|
||||
|
||||
import {
|
||||
GIVEAWAY_BOOST_PER_PREMIUM,
|
||||
@ -17,7 +18,6 @@ import {
|
||||
} from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
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 { getBoostProgressInfo } from '../../common/helpers/boostInfo';
|
||||
|
||||
@ -56,6 +56,19 @@ const GIVEAWAY_IMG_LIST: { [key: number]: string } = {
|
||||
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 = ({
|
||||
boostStatistics,
|
||||
isGiveawayAvailable,
|
||||
@ -199,6 +212,18 @@ const BoostStatistics = ({
|
||||
const renderBoostList = useLastCallback((boost) => {
|
||||
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 (
|
||||
<ListItem
|
||||
className="chat-item-clickable"
|
||||
@ -208,8 +233,7 @@ const BoostStatistics = ({
|
||||
<PrivateChatInfo
|
||||
className={styles.user}
|
||||
userId={boost.userId}
|
||||
customPeer={hasStars ? { ...CUSTOM_PEER_STAR, titleValue: boost.stars }
|
||||
: (!boost.userId ? CUSTOM_PEER_TO_BE_DISTRIBUTED : undefined)}
|
||||
customPeer={customPeer}
|
||||
status={lang('BoostExpireOn', formatDateAtTime(lang, boost.expires * 1000))}
|
||||
noEmojiStatus
|
||||
forceShowSelf
|
||||
|
||||
@ -29,6 +29,7 @@ type OwnProps = {
|
||||
noFastList?: boolean;
|
||||
cacheBuster?: any;
|
||||
beforeChildren?: React.ReactNode;
|
||||
scrollContainerClosest?: string;
|
||||
children: React.ReactNode;
|
||||
onLoadMore?: ({ direction }: { direction: LoadMoreDirection; noScroll?: boolean }) => void;
|
||||
onScroll?: (e: UIEvent<HTMLDivElement>) => void;
|
||||
@ -61,6 +62,7 @@ const InfiniteScroll: FC<OwnProps> = ({
|
||||
cacheBuster,
|
||||
beforeChildren,
|
||||
children,
|
||||
scrollContainerClosest,
|
||||
onLoadMore,
|
||||
onScroll,
|
||||
onWheel,
|
||||
@ -100,8 +102,10 @@ const InfiniteScroll: FC<OwnProps> = ({
|
||||
|
||||
// Initial preload
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!loadMoreBackwards || !container) {
|
||||
const scrollContainer = scrollContainerClosest
|
||||
? containerRef.current!.closest<HTMLDivElement>(scrollContainerClosest)!
|
||||
: containerRef.current!;
|
||||
if (!loadMoreBackwards || !scrollContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -110,16 +114,21 @@ const InfiniteScroll: FC<OwnProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollHeight, clientHeight } = container;
|
||||
const { scrollHeight, clientHeight } = scrollContainer;
|
||||
if (clientHeight && scrollHeight < clientHeight) {
|
||||
loadMoreBackwards();
|
||||
}
|
||||
}, [items, loadMoreBackwards, preloadBackwards]);
|
||||
}, [items, loadMoreBackwards, preloadBackwards, scrollContainerClosest]);
|
||||
|
||||
// Restore `scrollTop` after adding items
|
||||
useLayoutEffect(() => {
|
||||
const scrollContainer = scrollContainerClosest
|
||||
? containerRef.current!.closest<HTMLDivElement>(scrollContainerClosest)!
|
||||
: containerRef.current!;
|
||||
|
||||
const container = containerRef.current!;
|
||||
|
||||
requestForcedReflow(() => {
|
||||
const container = containerRef.current!;
|
||||
const state = stateRef.current;
|
||||
|
||||
state.listItemElements = container.querySelectorAll<HTMLDivElement>(itemSelector);
|
||||
@ -127,7 +136,7 @@ const InfiniteScroll: FC<OwnProps> = ({
|
||||
let newScrollTop: number;
|
||||
|
||||
if (state.currentAnchor && Array.from(state.listItemElements).includes(state.currentAnchor)) {
|
||||
const { scrollTop } = container;
|
||||
const { scrollTop } = scrollContainer;
|
||||
const newAnchorTop = state.currentAnchor!.getBoundingClientRect().top;
|
||||
newScrollTop = scrollTop + (newAnchorTop - state.currentAnchorTop!);
|
||||
} else {
|
||||
@ -142,18 +151,21 @@ const InfiniteScroll: FC<OwnProps> = ({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { scrollTop } = container;
|
||||
const { scrollTop } = scrollContainer;
|
||||
if (noScrollRestoreOnTop && scrollTop === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return () => {
|
||||
resetScroll(container, newScrollTop);
|
||||
resetScroll(scrollContainer, newScrollTop);
|
||||
|
||||
state.isScrollTopJustUpdated = true;
|
||||
};
|
||||
});
|
||||
}, [items, itemSelector, noScrollRestore, noScrollRestoreOnTop, cacheBuster, withAbsolutePositioning]);
|
||||
}, [
|
||||
items, itemSelector, noScrollRestore, noScrollRestoreOnTop, cacheBuster, withAbsolutePositioning,
|
||||
scrollContainerClosest,
|
||||
]);
|
||||
|
||||
const handleScroll = useLastCallback((e: UIEvent<HTMLDivElement>) => {
|
||||
if (loadMoreForwards && loadMoreBackwards) {
|
||||
@ -168,8 +180,10 @@ const InfiniteScroll: FC<OwnProps> = ({
|
||||
}
|
||||
|
||||
const listLength = listItemElements.length;
|
||||
const container = containerRef.current!;
|
||||
const { scrollTop, scrollHeight, offsetHeight } = container;
|
||||
const scrollContainer = scrollContainerClosest
|
||||
? containerRef.current!.closest<HTMLDivElement>(scrollContainerClosest)!
|
||||
: containerRef.current!;
|
||||
const { scrollTop, scrollHeight, offsetHeight } = scrollContainer;
|
||||
const top = listLength ? listItemElements[0].offsetTop : 0;
|
||||
const isNearTop = scrollTop <= top + sensitiveArea;
|
||||
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 (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={className}
|
||||
onScroll={handleScroll}
|
||||
onWheel={onWheel}
|
||||
teactFastList={!noFastList && !withAbsolutePositioning}
|
||||
onKeyDown={onKeyDown}
|
||||
|
||||
@ -51,7 +51,7 @@ export const MEDIA_PROGRESSIVE_CACHE_DISABLED = false;
|
||||
export const MEDIA_PROGRESSIVE_CACHE_NAME = 'tt-media-progressive';
|
||||
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
|
||||
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 AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500];
|
||||
export const DATA_BROADCAST_CHANNEL_NAME = 'tt-global';
|
||||
|
||||
@ -25,6 +25,7 @@ import {
|
||||
RE_TG_LINK,
|
||||
SAVED_FOLDER_ID,
|
||||
SERVICE_NOTIFICATIONS_USER_ID,
|
||||
STARS_CURRENCY_CODE,
|
||||
TME_WEB_DOMAINS,
|
||||
TMP_CHAT_ID,
|
||||
TOP_CHAT_MESSAGES_PRELOAD_LIMIT,
|
||||
@ -56,8 +57,10 @@ import {
|
||||
} from '../../index';
|
||||
import {
|
||||
addChatMembers,
|
||||
addChats,
|
||||
addMessages,
|
||||
addSimilarChannels,
|
||||
addUsers,
|
||||
addUserStatuses,
|
||||
deleteChatMessages,
|
||||
deletePeerPhoto,
|
||||
@ -466,7 +469,7 @@ addActionHandler('openThread', 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);
|
||||
if (!chat) {
|
||||
return;
|
||||
@ -534,7 +537,7 @@ addActionHandler('loadAllChats', async (global, actions, payload): Promise<void>
|
||||
addActionHandler('loadFullChat', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
chatId, force, withPhotos,
|
||||
} = payload!;
|
||||
} = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) {
|
||||
return;
|
||||
@ -716,7 +719,7 @@ addActionHandler('createChannel', 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);
|
||||
if (!chat) {
|
||||
return;
|
||||
@ -758,7 +761,7 @@ addActionHandler('deleteChatUser', (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);
|
||||
if (!chat) {
|
||||
return;
|
||||
@ -775,7 +778,7 @@ addActionHandler('deleteChat', (global, actions, payload): ActionReturnType => {
|
||||
});
|
||||
|
||||
addActionHandler('leaveChannel', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, tabId = getCurrentTabId() } = payload!;
|
||||
const { chatId, tabId = getCurrentTabId() } = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) {
|
||||
return;
|
||||
@ -797,8 +800,6 @@ addActionHandler('leaveChannel', async (global, actions, payload): Promise<void>
|
||||
global = deleteChatMessages(global, chatId, localMessageIds);
|
||||
setGlobal(global);
|
||||
}
|
||||
|
||||
actions.loadFullChat({ chatId, force: true });
|
||||
});
|
||||
|
||||
addActionHandler('deleteChannel', (global, actions, payload): ActionReturnType => {
|
||||
@ -891,7 +892,7 @@ addActionHandler('createGroupChat', async (global, actions, payload): Promise<vo
|
||||
});
|
||||
|
||||
addActionHandler('toggleChatPinned', (global, actions, payload): ActionReturnType => {
|
||||
const { id, folderId, tabId = getCurrentTabId() } = payload!;
|
||||
const { id, folderId, tabId = getCurrentTabId() } = payload;
|
||||
const chat = selectChat(global, id);
|
||||
if (!chat) {
|
||||
return;
|
||||
@ -938,7 +939,7 @@ addActionHandler('toggleChatPinned', (global, actions, payload): ActionReturnTyp
|
||||
});
|
||||
|
||||
addActionHandler('toggleChatArchived', (global, actions, payload): ActionReturnType => {
|
||||
const { id } = payload!;
|
||||
const { id } = payload;
|
||||
const chat = selectChat(global, id);
|
||||
if (chat) {
|
||||
void callApi('toggleChatArchived', {
|
||||
@ -949,7 +950,7 @@ addActionHandler('toggleChatArchived', (global, actions, payload): ActionReturnT
|
||||
});
|
||||
|
||||
addActionHandler('toggleSavedDialogPinned', (global, actions, payload): ActionReturnType => {
|
||||
const { id, tabId = getCurrentTabId() } = payload!;
|
||||
const { id, tabId = getCurrentTabId() } = payload;
|
||||
const chat = selectChat(global, id);
|
||||
if (!chat) {
|
||||
return;
|
||||
@ -1046,7 +1047,7 @@ addActionHandler('editChatFolders', (global, actions, payload): ActionReturnType
|
||||
});
|
||||
|
||||
addActionHandler('editChatFolder', (global, actions, payload): ActionReturnType => {
|
||||
const { id, folderUpdate } = payload!;
|
||||
const { id, folderUpdate } = payload;
|
||||
const folder = selectChatFolder(global, id);
|
||||
|
||||
if (folder) {
|
||||
@ -1063,7 +1064,7 @@ addActionHandler('editChatFolder', (global, actions, payload): ActionReturnType
|
||||
});
|
||||
|
||||
addActionHandler('addChatFolder', async (global, actions, payload): Promise<void> => {
|
||||
const { folder, tabId = getCurrentTabId() } = payload!;
|
||||
const { folder, tabId = getCurrentTabId() } = payload;
|
||||
const { orderedIds, byId } = global.chatFolders;
|
||||
|
||||
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> => {
|
||||
const { folderIds } = payload!;
|
||||
const { folderIds } = payload;
|
||||
|
||||
const result = await callApi('sortChatFolders', folderIds);
|
||||
if (result) {
|
||||
@ -1191,21 +1192,65 @@ addActionHandler('markTopicRead', (global, actions, payload): ActionReturnType =
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('openChatByInvite', async (global, actions, payload): Promise<void> => {
|
||||
const { hash, tabId = getCurrentTabId() } = payload!;
|
||||
addActionHandler('checkChatInvite', async (global, actions, payload): Promise<void> => {
|
||||
const { hash, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
const result = await callApi('openChatByInvite', hash);
|
||||
const result = await callApi('checkChatInvite', hash);
|
||||
if (!result) {
|
||||
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> => {
|
||||
const {
|
||||
phoneNumber, startAttach, attach, text, tabId = getCurrentTabId(),
|
||||
} = payload!;
|
||||
} = payload;
|
||||
|
||||
// Open temporary empty chat to make the click response feel faster
|
||||
actions.openChat({ id: TMP_CHAT_ID, tabId });
|
||||
@ -1240,7 +1285,7 @@ addActionHandler('openTelegramLink', async (global, actions, payload): Promise<v
|
||||
|
||||
const {
|
||||
openChatByPhoneNumber,
|
||||
openChatByInvite,
|
||||
checkChatInvite,
|
||||
openStickerSet,
|
||||
openChatWithDraft,
|
||||
joinVoiceChatByLink,
|
||||
@ -1313,7 +1358,7 @@ addActionHandler('openTelegramLink', async (global, actions, payload): Promise<v
|
||||
}
|
||||
|
||||
if (hash) {
|
||||
openChatByInvite({ hash, tabId });
|
||||
checkChatInvite({ hash, tabId });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1453,8 +1498,8 @@ addActionHandler('processBoostParameters', async (global, actions, payload): Pro
|
||||
});
|
||||
});
|
||||
|
||||
addActionHandler('acceptInviteConfirmation', async (global, actions, payload): Promise<void> => {
|
||||
const { hash, tabId = getCurrentTabId() } = payload!;
|
||||
addActionHandler('acceptChatInvite', async (global, actions, payload): Promise<void> => {
|
||||
const { hash, tabId = getCurrentTabId() } = payload;
|
||||
const result = await callApi('importChatInvite', { hash });
|
||||
if (!result) {
|
||||
return;
|
||||
@ -1467,7 +1512,7 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
|
||||
const {
|
||||
username, messageId, commentId, startParam, startAttach, attach, threadId, originalParts, startApp, text,
|
||||
tabId = getCurrentTabId(),
|
||||
} = payload!;
|
||||
} = payload;
|
||||
|
||||
const chat = selectCurrentChat(global, tabId);
|
||||
const webAppName = originalParts?.[1];
|
||||
@ -1557,7 +1602,7 @@ addActionHandler('togglePreHistoryHidden', async (global, actions, payload): Pro
|
||||
const {
|
||||
chatId, isEnabled,
|
||||
tabId = getCurrentTabId(),
|
||||
} = payload!;
|
||||
} = payload;
|
||||
|
||||
const chat = await ensureIsSuperGroup(global, actions, chatId, tabId);
|
||||
if (!chat) {
|
||||
@ -1572,7 +1617,7 @@ addActionHandler('togglePreHistoryHidden', async (global, actions, payload): Pro
|
||||
});
|
||||
|
||||
addActionHandler('updateChatDefaultBannedRights', (global, actions, payload): ActionReturnType => {
|
||||
const { chatId, bannedRights } = payload!;
|
||||
const { chatId, bannedRights } = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
|
||||
if (!chat) {
|
||||
@ -1586,7 +1631,7 @@ addActionHandler('updateChatMemberBannedRights', async (global, actions, payload
|
||||
const {
|
||||
chatId, userId, bannedRights,
|
||||
tabId = getCurrentTabId(),
|
||||
} = payload!;
|
||||
} = payload;
|
||||
|
||||
const user = selectUser(global, userId);
|
||||
|
||||
@ -1634,7 +1679,7 @@ addActionHandler('updateChatAdmin', async (global, actions, payload): Promise<vo
|
||||
const {
|
||||
chatId, userId, adminRights, customTitle,
|
||||
tabId = getCurrentTabId(),
|
||||
} = payload!;
|
||||
} = payload;
|
||||
|
||||
const user = selectUser(global, userId);
|
||||
if (!user) {
|
||||
|
||||
@ -14,10 +14,10 @@ import { isChatChannel, isChatSuperGroup } from '../../helpers';
|
||||
import {
|
||||
getPrizeStarsTransactionFromGiveaway,
|
||||
getRequestInputInvoice,
|
||||
getStarsTransactionFromGift,
|
||||
} from '../../helpers/payments';
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
import {
|
||||
appendStarsSubscriptions,
|
||||
appendStarsTransactions, closeInvoice,
|
||||
openStarsTransactionFromReceipt,
|
||||
openStarsTransactionModal,
|
||||
@ -38,6 +38,7 @@ import {
|
||||
selectChatMessage,
|
||||
selectPaymentFormId,
|
||||
selectPaymentInputInvoice, selectPaymentRequestId,
|
||||
selectPeer,
|
||||
selectProviderPublicToken,
|
||||
selectProviderPublishableKey,
|
||||
selectSmartGlocalCredentials,
|
||||
@ -271,6 +272,10 @@ addActionHandler('sendStarPaymentForm', async (global, actions, payload): Promis
|
||||
global = closeInvoice(global, tabId);
|
||||
setGlobal(global);
|
||||
|
||||
if ('channelId' in result) {
|
||||
actions.openChat({ id: result.channelId, tabId });
|
||||
}
|
||||
|
||||
actions.apiUpdate({
|
||||
'@type': 'updatePaymentStateCompleted',
|
||||
inputInvoice,
|
||||
@ -535,22 +540,6 @@ addActionHandler('closeStarsGiftingModal', (global, actions, payload): ActionRet
|
||||
}, 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 => {
|
||||
const {
|
||||
chatId,
|
||||
@ -1056,11 +1045,18 @@ addActionHandler('loadStarStatus', async (global): Promise<void> => {
|
||||
inbound: undefined,
|
||||
outbound: undefined,
|
||||
},
|
||||
subscriptions: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
@ -1089,3 +1085,54 @@ addActionHandler('loadStarsTransactions', async (global, actions, payload): Prom
|
||||
}
|
||||
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();
|
||||
});
|
||||
|
||||
@ -125,8 +125,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
const chat = selectChat(global, update.id);
|
||||
if (chat && isChatChannel(chat)) {
|
||||
const chatMessages = selectChatMessages(global, update.id);
|
||||
const localMessageIds = Object.keys(chatMessages).map(Number).filter(isLocalMessageId);
|
||||
global = deleteChatMessages(global, chat.id, localMessageIds);
|
||||
if (chatMessages) {
|
||||
const localMessageIds = Object.keys(chatMessages).map(Number).filter(isLocalMessageId);
|
||||
global = deleteChatMessages(global, chat.id, localMessageIds);
|
||||
}
|
||||
}
|
||||
|
||||
return global;
|
||||
@ -434,16 +436,6 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
return global;
|
||||
}
|
||||
|
||||
case 'showInvite': {
|
||||
const { data } = update;
|
||||
|
||||
Object.values(global.byTabId).forEach(({ id: tabId }) => {
|
||||
actions.showDialog({ data, tabId });
|
||||
});
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
case 'updatePendingJoinRequests': {
|
||||
const { chatId, requestsPending, recentRequesterIds } = update;
|
||||
const chat = global.chats.byId[chatId];
|
||||
|
||||
@ -176,7 +176,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
}
|
||||
|
||||
case 'updatePaidReactionPrivacy': {
|
||||
return {
|
||||
global = {
|
||||
...global,
|
||||
settings: {
|
||||
...global.settings,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { ActionReturnType } from '../../types';
|
||||
|
||||
import { STARS_CURRENCY_CODE } from '../../../config';
|
||||
import { areDeepEqual } from '../../../util/areDeepEqual';
|
||||
import { formatCurrencyAsString } from '../../../util/formatCurrency';
|
||||
import * as langProvider from '../../../util/oldLangProvider';
|
||||
@ -19,13 +20,15 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
if (invoice) {
|
||||
const { amount, currency, title } = invoice;
|
||||
|
||||
actions.showNotification({
|
||||
tabId,
|
||||
message: langProvider.oldTranslate('PaymentInfoHint', [
|
||||
formatCurrencyAsString(amount, currency, langProvider.getTranslationFn().code),
|
||||
title,
|
||||
]),
|
||||
});
|
||||
if (currency !== STARS_CURRENCY_CODE) {
|
||||
actions.showNotification({
|
||||
tabId,
|
||||
message: langProvider.oldTranslate('PaymentInfoHint', [
|
||||
formatCurrencyAsString(amount, currency, langProvider.getTranslationFn().code),
|
||||
title,
|
||||
]),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (inputInvoice?.type === 'giftcode') {
|
||||
@ -42,7 +45,6 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
isCompleted: true,
|
||||
},
|
||||
}, tabId);
|
||||
global = closeInvoice(global, tabId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,14 +62,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
isCompleted: true,
|
||||
},
|
||||
}, tabId);
|
||||
global = closeInvoice(global, tabId);
|
||||
}
|
||||
}
|
||||
|
||||
if (inputInvoice?.type === 'stars') {
|
||||
if (!inputInvoice.stars) {
|
||||
return;
|
||||
}
|
||||
const starsModalState = selectTabState(global, tabId).starsGiftModal;
|
||||
|
||||
if (starsModalState && starsModalState.isOpen) {
|
||||
@ -77,10 +75,27 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
isCompleted: true,
|
||||
},
|
||||
}, 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);
|
||||
});
|
||||
|
||||
|
||||
@ -204,3 +204,10 @@ addActionHandler('requestChatTranslation', (global, actions, payload): ActionRet
|
||||
const { chatId, toLanguageCode, tabId = getCurrentTabId() } = payload;
|
||||
return updateRequestedChatTranslation(global, chatId, toLanguageCode, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('closeChatInviteModal', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
return updateTabState(global, {
|
||||
chatInviteModal: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import type { ActionReturnType } from '../../types';
|
||||
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { getStarsTransactionFromGift } from '../../helpers/payments';
|
||||
import { addActionHandler } from '../../index';
|
||||
import {
|
||||
clearPayment, closeInvoice, openStarsTransactionModal, updatePayment,
|
||||
} from '../../reducers';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import { selectTabState } from '../../selectors';
|
||||
import { selectChatMessage, selectTabState } from '../../selectors';
|
||||
|
||||
addActionHandler('closePaymentModal', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
@ -97,6 +98,22 @@ addActionHandler('openStarsTransactionModal', (global, actions, payload): Action
|
||||
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 => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
@ -104,3 +121,21 @@ addActionHandler('closeStarsTransactionModal', (global, actions, payload): Actio
|
||||
starsTransactionModal: undefined,
|
||||
}, 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);
|
||||
});
|
||||
|
||||
@ -4,12 +4,15 @@ import type {
|
||||
ApiChatBannedRights,
|
||||
ApiChatFolder,
|
||||
ApiChatFullInfo,
|
||||
ApiChatInviteInfo,
|
||||
ApiPeer,
|
||||
ApiTopic,
|
||||
ApiUser,
|
||||
} from '../../api/types';
|
||||
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 {
|
||||
@ -482,3 +485,16 @@ export function getGroupStatus(lang: LangFn, chat: ApiChat) {
|
||||
? lang('Subscribers', 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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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') {
|
||||
const chat = selectChat(global, inputInvoice.chatId);
|
||||
if (!chat) {
|
||||
|
||||
@ -385,6 +385,7 @@ export function leaveChat<T extends GlobalState>(global: T, leftChatId: string):
|
||||
global = removeChatFromChatLists(global, leftChatId);
|
||||
|
||||
global = updateChat(global, leftChatId, { isNotJoined: true });
|
||||
global = updateChatFullInfo(global, leftChatId, { joinInfo: undefined });
|
||||
|
||||
return global;
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import type {
|
||||
ApiInvoice, ApiPaymentForm,
|
||||
ApiReceiptRegular,
|
||||
ApiReceiptStars,
|
||||
ApiStarsSubscription,
|
||||
ApiStarsTransaction,
|
||||
} from '../../api/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>(
|
||||
global: T, transaction: ApiStarsTransaction, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): T {
|
||||
|
||||
@ -14,6 +14,7 @@ import type {
|
||||
ApiChatBannedRights,
|
||||
ApiChatFolder,
|
||||
ApiChatFullInfo,
|
||||
ApiChatInviteInfo,
|
||||
ApiChatlistExportedInvite,
|
||||
ApiChatlistInvite,
|
||||
ApiChatReactions,
|
||||
@ -33,7 +34,6 @@ import type {
|
||||
ApiGroupStatistics,
|
||||
ApiInputInvoice,
|
||||
ApiInputMessageReplyInfo,
|
||||
ApiInviteInfo,
|
||||
ApiInvoice,
|
||||
ApiKeyboardButton,
|
||||
ApiMediaFormat,
|
||||
@ -65,6 +65,7 @@ import type {
|
||||
ApiSession,
|
||||
ApiSessionData,
|
||||
ApiSponsoredMessage, ApiStarGiveawayOption,
|
||||
ApiStarsSubscription,
|
||||
ApiStarsTransaction,
|
||||
ApiStarTopupOption,
|
||||
ApiStealthMode,
|
||||
@ -183,6 +184,10 @@ export type StarsTransactionHistory = Record<StarsTransactionType, {
|
||||
transactions: ApiStarsTransaction[];
|
||||
nextOffset?: string;
|
||||
} | undefined>;
|
||||
export type StarsSubscriptions = {
|
||||
list: ApiStarsSubscription[];
|
||||
nextOffset?: string;
|
||||
};
|
||||
|
||||
export type ConfettiStyle = 'poppers' | 'top-down';
|
||||
|
||||
@ -351,6 +356,11 @@ export type TabState = {
|
||||
messageIds: number[];
|
||||
};
|
||||
|
||||
chatInviteModal?: {
|
||||
hash: string;
|
||||
inviteInfo: ApiChatInviteInfo;
|
||||
};
|
||||
|
||||
seenByModal?: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
@ -595,7 +605,7 @@ export type TabState = {
|
||||
};
|
||||
|
||||
notifications: ApiNotification[];
|
||||
dialogs: (ApiError | ApiInviteInfo | ApiContact)[];
|
||||
dialogs: (ApiError | ApiContact)[];
|
||||
|
||||
safeLinkModalUrl?: string;
|
||||
mapModal?: {
|
||||
@ -748,6 +758,9 @@ export type TabState = {
|
||||
starsTransactionModal?: {
|
||||
transaction: ApiStarsTransaction;
|
||||
};
|
||||
starsSubscriptionModal?: {
|
||||
subscription: ApiStarsSubscription;
|
||||
};
|
||||
|
||||
giftModal?: {
|
||||
isCompleted?: boolean;
|
||||
@ -1216,6 +1229,7 @@ export type GlobalState = {
|
||||
topupOptions: ApiStarTopupOption[];
|
||||
balance: number;
|
||||
history: StarsTransactionHistory;
|
||||
subscriptions?: StarsSubscriptions;
|
||||
};
|
||||
};
|
||||
|
||||
@ -1337,7 +1351,12 @@ export interface ActionPayloads {
|
||||
adminRights: ApiChatAdminRights;
|
||||
customTitle?: string;
|
||||
} & WithTabId;
|
||||
acceptInviteConfirmation: { hash: string } & WithTabId;
|
||||
|
||||
checkChatInvite: {
|
||||
hash: string;
|
||||
} & WithTabId;
|
||||
acceptChatInvite: { hash: string } & WithTabId;
|
||||
closeChatInviteModal: WithTabId | undefined;
|
||||
|
||||
// settings
|
||||
setSettingOption: Partial<ISettings> | undefined;
|
||||
@ -1546,9 +1565,6 @@ export interface ActionPayloads {
|
||||
attach?: string;
|
||||
text?: string;
|
||||
} & WithTabId;
|
||||
openChatByInvite: {
|
||||
hash: string;
|
||||
} & WithTabId;
|
||||
toggleSavedDialogPinned: {
|
||||
id: string;
|
||||
} & WithTabId;
|
||||
@ -1839,6 +1855,10 @@ export interface ActionPayloads {
|
||||
messageId: number;
|
||||
} & WithTabId;
|
||||
closeStarsTransactionModal: WithTabId | undefined;
|
||||
openStarsSubscriptionModal: {
|
||||
subscription: ApiStarsSubscription;
|
||||
} & WithTabId;
|
||||
closeStarsSubscriptionModal: WithTabId | undefined;
|
||||
openPrizeStarsTransactionFromGiveaway: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
@ -2304,6 +2324,16 @@ export interface ActionPayloads {
|
||||
loadStarsTransactions: {
|
||||
type: StarsTransactionType;
|
||||
};
|
||||
loadStarsSubscriptions: undefined;
|
||||
changeStarsSubscription: {
|
||||
peerId?: string;
|
||||
id: string;
|
||||
isCancelled: boolean;
|
||||
};
|
||||
fulfillStarsSubscription: {
|
||||
peerId?: string;
|
||||
id: string;
|
||||
};
|
||||
openStarsBalanceModal: {
|
||||
originPayment?: TabState['payment'];
|
||||
originReaction?: {
|
||||
|
||||
@ -1000,8 +1000,11 @@ class TelegramClient {
|
||||
if (isExported) this.releaseExportedSender(sender);
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (e instanceof errors.ServerError || e.message === 'RPC_CALL_FAIL'
|
||||
|| e.message === 'RPC_MCGET_FAIL') {
|
||||
if (e instanceof errors.ServerError
|
||||
|| 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}`);
|
||||
await sleep(2000);
|
||||
} else if (e instanceof errors.FloodWaitError || e instanceof errors.FloodTestPhoneWaitError) {
|
||||
|
||||
@ -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.getStarsTransactionsByID#27842d2e peer:InputPeer id:Vector<InputStarsTransaction> = payments.StarsStatus;
|
||||
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>;
|
||||
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;
|
||||
|
||||
@ -368,6 +368,9 @@
|
||||
"payments.getStarsStatus",
|
||||
"payments.getStarsTransactions",
|
||||
"payments.getStarsTransactionsByID",
|
||||
"payments.getStarsSubscriptions",
|
||||
"payments.changeStarsSubscription",
|
||||
"payments.fulfillStarsSubscription",
|
||||
"payments.sendStarsForm",
|
||||
"payments.getStarsGiftOptions",
|
||||
"payments.getStarsGiveawayOptions",
|
||||
|
||||
@ -33,8 +33,12 @@
|
||||
mask-image: linear-gradient(to right, transparent, black $borderStart, black $borderEnd, transparent);
|
||||
}
|
||||
|
||||
@mixin gradient-border-left($indent) {
|
||||
mask-image: linear-gradient(to right, transparent, black $indent);
|
||||
@mixin gradient-border-left($indent, $cutout: 0px) {
|
||||
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) {
|
||||
|
||||
@ -9,6 +9,7 @@ import type {
|
||||
ApiChatInviteImporter,
|
||||
ApiDocument,
|
||||
ApiExportedInvite,
|
||||
ApiFakeType,
|
||||
ApiLanguage,
|
||||
ApiMessage,
|
||||
ApiPhoto,
|
||||
@ -517,19 +518,25 @@ export type InlineBotSettings = {
|
||||
export type CustomPeerType = 'premium' | 'toBeDistributed' | 'contacts' | 'nonContacts'
|
||||
| 'groups' | 'channels' | 'bots' | 'excludeMuted' | 'excludeArchived' | 'excludeRead' | 'stars';
|
||||
|
||||
export interface CustomPeer {
|
||||
export type CustomPeer = {
|
||||
isCustomPeer: true;
|
||||
key?: string | number;
|
||||
titleKey: string;
|
||||
subtitleKey?: string;
|
||||
avatarIcon: IconName;
|
||||
avatarIcon?: IconName;
|
||||
isAvatarSquare?: boolean;
|
||||
titleValue?: number;
|
||||
peerColorId?: number;
|
||||
isVerified?: boolean;
|
||||
fakeType?: ApiFakeType;
|
||||
customPeerAvatarColor?: string;
|
||||
withPremiumGradient?: boolean;
|
||||
}
|
||||
} & ({
|
||||
titleKey: string;
|
||||
title?: undefined;
|
||||
} | {
|
||||
title: string;
|
||||
titleKey?: undefined;
|
||||
});
|
||||
|
||||
export interface UniqueCustomPeer extends CustomPeer {
|
||||
type: CustomPeerType;
|
||||
}
|
||||
export type UniqueCustomPeer<T = CustomPeerType> = CustomPeer & {
|
||||
type: T;
|
||||
};
|
||||
|
||||
17
src/types/language.d.ts
vendored
17
src/types/language.d.ts
vendored
@ -1533,9 +1533,11 @@ export interface LangPair {
|
||||
'user': string | number;
|
||||
'link': string | number;
|
||||
};
|
||||
'CreditsBoxOutAbout': {
|
||||
'StarsTransactionTOS': {
|
||||
'link': string | number;
|
||||
};
|
||||
'StarsTransactionTOSLinkText': undefined;
|
||||
'StarsTransactionTOSLink': undefined;
|
||||
'GiftStarsOutgoing': {
|
||||
'user': string | number;
|
||||
};
|
||||
@ -1549,10 +1551,23 @@ export interface LangPair {
|
||||
'StarsReactionLink': undefined;
|
||||
'MiniAppsMoreTabs': {
|
||||
'botName': string | number;
|
||||
'count': string | number;
|
||||
};
|
||||
'PrizeCredits': {
|
||||
'count': string | number;
|
||||
};
|
||||
'StarsSubscribeText': {
|
||||
'chat': string | number;
|
||||
'amount': string | number;
|
||||
};
|
||||
'StarsSubscribeInfo': {
|
||||
'link': string | number;
|
||||
};
|
||||
'StarsSubscribeInfoLinkText': undefined;
|
||||
'StarsSubscribeInfoLink': undefined;
|
||||
'StarsPerMonth': {
|
||||
'amount': string | number;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -59,7 +59,7 @@ export const processDeepLink = (url: string): boolean => {
|
||||
const params = Object.fromEntries(searchParams);
|
||||
|
||||
const {
|
||||
openChatByInvite,
|
||||
checkChatInvite,
|
||||
openChatByUsername,
|
||||
openChatByPhoneNumber,
|
||||
openStickerSet,
|
||||
@ -139,7 +139,7 @@ export const processDeepLink = (url: string): boolean => {
|
||||
case 'join': {
|
||||
const { invite } = params;
|
||||
|
||||
openChatByInvite({ hash: invite });
|
||||
checkChatInvite({ hash: invite });
|
||||
break;
|
||||
}
|
||||
case 'addemoji':
|
||||
|
||||
@ -6,6 +6,7 @@ export enum Bundles {
|
||||
Main,
|
||||
Extra,
|
||||
Calls,
|
||||
Stars,
|
||||
}
|
||||
|
||||
interface ImportedBundles {
|
||||
@ -13,6 +14,7 @@ interface ImportedBundles {
|
||||
[Bundles.Main]: typeof import('../bundles/main');
|
||||
[Bundles.Extra]: typeof import('../bundles/extra');
|
||||
[Bundles.Calls]: typeof import('../bundles/calls');
|
||||
[Bundles.Stars]: typeof import('../bundles/stars');
|
||||
}
|
||||
|
||||
type BundlePromises = {
|
||||
@ -46,6 +48,9 @@ export async function loadBundle<B extends Bundles>(bundleName: B) {
|
||||
case Bundles.Calls:
|
||||
LOAD_PROMISES[Bundles.Calls] = import(/* webpackChunkName: "BundleCalls" */ '../bundles/calls');
|
||||
break;
|
||||
case Bundles.Stars:
|
||||
LOAD_PROMISES[Bundles.Stars] = import(/* webpackChunkName: "BundleStars" */ '../bundles/stars');
|
||||
break;
|
||||
}
|
||||
|
||||
(LOAD_PROMISES[bundleName] as Promise<ImportedBundles[B]>).then(runCallbacks);
|
||||
|
||||
@ -10,22 +10,6 @@ export const CUSTOM_PEER_PREMIUM: UniqueCustomPeer = {
|
||||
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[] = [
|
||||
{
|
||||
isCustomPeer: true,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user