Support subscription invites (#5024)

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

View File

@ -8,6 +8,7 @@ import type {
ApiChatBannedRights,
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(),
};
}

View File

@ -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,

View File

@ -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);

View File

@ -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[]) {

View File

@ -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());

View File

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

View File

@ -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';

View File

@ -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;

View File

@ -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 {

View File

@ -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 |

View File

@ -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"

View File

@ -18,19 +18,14 @@ export { default as AttachBotInstallModal } from '../components/modals/attachBot
export { default as DeleteFolderDialog } from '../components/main/DeleteFolderDialog';
export { default as 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
View File

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

View File

@ -220,6 +220,9 @@ const Avatar: FC<OwnProps> = ({
} else if (chat) {
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);
}

View File

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

View File

@ -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}

View File

@ -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>

View File

@ -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)}`;

View File

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

View File

@ -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);

View File

@ -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}

View File

@ -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);
}

View File

@ -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;

View File

@ -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;

View File

@ -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>;

View File

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

View File

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

View File

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

View File

@ -13,56 +13,28 @@
}
.value {
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 {

View File

@ -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>

View File

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

View File

@ -69,6 +69,7 @@ const GiftCodeModal = ({
const header = (
<>
<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}

View File

@ -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;

View File

@ -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;

View File

@ -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);
}

View File

@ -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}

View File

@ -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;

View File

@ -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));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,9 @@
@use '../../../../styles/mixins';
.root {
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));
}

View File

@ -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>
);

View File

@ -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;

View File

@ -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);
}

View File

@ -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}
/>

View File

@ -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

View File

@ -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}

View File

@ -51,7 +51,7 @@ export const MEDIA_PROGRESSIVE_CACHE_DISABLED = false;
export const MEDIA_PROGRESSIVE_CACHE_NAME = 'tt-media-progressive';
export const MEDIA_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';

View File

@ -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) {

View File

@ -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();
});

View File

@ -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];

View File

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

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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,
};
}

View File

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

View File

@ -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;
}

View File

@ -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 {

View File

@ -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?: {

View File

@ -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) {

View File

@ -1654,6 +1654,9 @@ payments.sendStarsForm#2bb731d flags:# form_id:long invoice:InputInvoice = payme
payments.refundStarsCharge#25ae8f4a user_id:InputUser charge_id:string = Updates;
payments.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;

View File

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

View File

@ -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) {

View File

@ -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;
};

View File

@ -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;
};
}

View File

@ -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':

View File

@ -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);

View File

@ -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,