Stars Gifting: Implement Stars Gifting for users (#4847)

This commit is contained in:
Alexander Zinchuk 2024-08-29 15:52:21 +02:00
parent 7d1eaa55da
commit 6f292c9032
49 changed files with 1668 additions and 387 deletions

View File

@ -352,6 +352,7 @@ function buildAction(
let phoneCall: PhoneCallAction | undefined; let phoneCall: PhoneCallAction | undefined;
let call: Partial<ApiGroupCall> | undefined; let call: Partial<ApiGroupCall> | undefined;
let amount: number | undefined; let amount: number | undefined;
let stars: number | undefined;
let currency: string | undefined; let currency: string | undefined;
let giftCryptoInfo: { let giftCryptoInfo: {
currency: string; currency: string;
@ -370,11 +371,11 @@ function buildAction(
let isUnclaimed: boolean | undefined; let isUnclaimed: boolean | undefined;
let pluralValue: number | undefined; let pluralValue: number | undefined;
const targetUserIds = 'users' in action let targetUserIds = 'users' in action
? action.users && action.users.map((id) => buildApiPeerId(id, 'user')) ? action.users && action.users.map((id) => buildApiPeerId(id, 'user'))
: ('userId' in action && [buildApiPeerId(action.userId, 'user')]) || []; : ('userId' in action && [buildApiPeerId(action.userId, 'user')]) || [];
let targetChatId: string | undefined;
let targetChatId;
if (action instanceof GramJs.MessageActionChatCreate) { if (action instanceof GramJs.MessageActionChatCreate) {
text = 'Notification.CreatedChatWithTitle'; text = 'Notification.CreatedChatWithTitle';
translationValues.push('%action_origin%', action.title); translationValues.push('%action_origin%', action.title);
@ -611,6 +612,27 @@ function buildAction(
text = 'ActionRefunded'; text = 'ActionRefunded';
amount = Number(action.totalAmount); amount = Number(action.totalAmount);
currency = action.currency; currency = action.currency;
} else if (action instanceof GramJs.MessageActionRequestedPeer) {
text = 'ActionRequestedPeer';
if (action.peers) {
targetUserIds = action.peers?.map((peer) => getApiChatIdFromMtpPeer(peer));
}
if (targetPeerId) {
translationValues.unshift('%action_origin%');
}
} else if (action instanceof GramJs.MessageActionGiftStars) {
text = isOutgoing ? 'ActionGiftOutbound' : 'BoostingReceivedGiftNoName';
if (isOutgoing) {
translationValues.push('%gift_payment_amount%');
} else {
translationValues.push('%action_origin%', '%gift_payment_amount%');
}
if (targetPeerId) {
targetUserIds.push(targetPeerId);
}
currency = action.currency;
amount = action.amount.toJSNumber();
stars = action.stars.toJSNumber();
} else { } else {
text = 'ChatList.UnsupportedMessage'; text = 'ChatList.UnsupportedMessage';
} }
@ -628,6 +650,7 @@ function buildAction(
targetChatId, targetChatId,
photo, // TODO Only used internally now, will be used for the UI in future photo, // TODO Only used internally now, will be used for the UI in future
amount, amount,
stars,
currency, currency,
giftCryptoInfo, giftCryptoInfo,
isGiveaway, isGiveaway,

View File

@ -8,7 +8,7 @@ import type {
ApiGiveawayInfo, ApiGiveawayInfo,
ApiInvoice, ApiLabeledPrice, ApiMyBoost, ApiPaymentCredentials, ApiInvoice, ApiLabeledPrice, ApiMyBoost, ApiPaymentCredentials,
ApiPaymentForm, ApiPaymentSavedInfo, ApiPremiumGiftCodeOption, ApiPremiumPromo, ApiPremiumSubscriptionOption, ApiPaymentForm, ApiPaymentSavedInfo, ApiPremiumGiftCodeOption, ApiPremiumPromo, ApiPremiumSubscriptionOption,
ApiReceipt, ApiReceipt, ApiStarsGiftOption,
ApiStarsTransaction, ApiStarsTransaction,
ApiStarsTransactionPeer, ApiStarsTransactionPeer,
ApiStarTopupOption, ApiStarTopupOption,
@ -383,6 +383,19 @@ export function buildApiPremiumGiftCodeOption(option: GramJs.PremiumGiftCodeOpti
}; };
} }
export function buildApiStarsGiftOptions(option: GramJs.StarsGiftOption): ApiStarsGiftOption {
const {
extended, stars, amount, currency,
} = option;
return {
isExtended: extended,
stars: stars.toJSNumber(),
amount: amount.toJSNumber(),
currency,
};
}
export function buildApiStarsTransactionPeer(peer: GramJs.TypeStarsTransactionPeer): ApiStarsTransactionPeer { export function buildApiStarsTransactionPeer(peer: GramJs.TypeStarsTransactionPeer): ApiStarsTransactionPeer {
if (peer instanceof GramJs.StarsTransactionPeerAppStore) { if (peer instanceof GramJs.StarsTransactionPeerAppStore) {
return { type: 'appStore' }; return { type: 'appStore' };

View File

@ -546,6 +546,15 @@ GramJs.TypeInputStorePaymentPurpose {
}); });
} }
if (purpose.type === 'starsgift') {
return new GramJs.InputStorePaymentStarsGift({
userId: buildInputEntity(purpose.user.id, purpose.user.accessHash) as GramJs.InputUser,
stars: BigInt(purpose.stars),
currency: purpose.currency,
amount: BigInt(purpose.amount),
});
}
if (purpose.type === 'giftcode') { if (purpose.type === 'giftcode') {
return new GramJs.InputStorePaymentPremiumGiftCode({ return new GramJs.InputStorePaymentPremiumGiftCode({
users: purpose.users.map((user) => buildInputEntity(user.id, user.accessHash) as GramJs.InputUser), users: purpose.users.map((user) => buildInputEntity(user.id, user.accessHash) as GramJs.InputUser),

View File

@ -101,7 +101,7 @@ export {
validateRequestedInfo, sendPaymentForm, getPaymentForm, getReceipt, fetchPremiumPromo, fetchTemporaryPaymentPassword, validateRequestedInfo, sendPaymentForm, getPaymentForm, getReceipt, fetchPremiumPromo, fetchTemporaryPaymentPassword,
applyBoost, fetchBoostList, fetchBoostStatus, fetchGiveawayInfo, fetchMyBoosts, applyGiftCode, checkGiftCode, applyBoost, fetchBoostList, fetchBoostStatus, fetchGiveawayInfo, fetchMyBoosts, applyGiftCode, checkGiftCode,
getPremiumGiftCodeOptions, launchPrepaidGiveaway, fetchStarsStatus, fetchStarsTopupOptions, fetchStarsTransactions, getPremiumGiftCodeOptions, launchPrepaidGiveaway, fetchStarsStatus, fetchStarsTopupOptions, fetchStarsTransactions,
sendStarPaymentForm, sendStarPaymentForm, getStarsGiftOptions,
} from './payments'; } from './payments';
export * from './fragment'; export * from './fragment';

View File

@ -19,7 +19,7 @@ import {
buildApiPaymentForm, buildApiPaymentForm,
buildApiPremiumGiftCodeOption, buildApiPremiumGiftCodeOption,
buildApiPremiumPromo, buildApiPremiumPromo,
buildApiReceipt, buildApiReceipt, buildApiStarsGiftOptions,
buildApiStarsTransaction, buildApiStarsTransaction,
buildApiStarTopupOption, buildApiStarTopupOption,
buildShippingOptions, buildShippingOptions,
@ -403,6 +403,22 @@ export async function getPremiumGiftCodeOptions({
return result.map(buildApiPremiumGiftCodeOption); return result.map(buildApiPremiumGiftCodeOption);
} }
export async function getStarsGiftOptions({
chat,
}: {
chat?: ApiChat;
}) {
const result = await invokeRequest(new GramJs.payments.GetStarsGiftOptions({
userId: chat && buildInputPeer(chat.id, chat.accessHash),
}));
if (!result) {
return undefined;
}
return result.map(buildApiStarsGiftOptions);
}
export function launchPrepaidGiveaway({ export function launchPrepaidGiveaway({
chat, chat,
giveawayId, giveawayId,

View File

@ -240,8 +240,16 @@ export type ApiInputInvoiceStars = {
amount: number; amount: number;
}; };
export type ApiInputInvoiceStarsGift = {
type: 'starsgift';
userId: string;
stars: number;
currency: string;
amount: number;
};
export type ApiInputInvoice = ApiInputInvoiceMessage | ApiInputInvoiceSlug | ApiInputInvoiceGiveaway export type ApiInputInvoice = ApiInputInvoiceMessage | ApiInputInvoiceSlug | ApiInputInvoiceGiveaway
| ApiInputInvoiceGiftCode | ApiInputInvoiceStars; | ApiInputInvoiceGiftCode | ApiInputInvoiceStarsGift | ApiInputInvoiceStars;
/* Used for Invoice request */ /* Used for Invoice request */
export type ApiRequestInputInvoiceMessage = { export type ApiRequestInputInvoiceMessage = {
@ -392,6 +400,8 @@ export interface ApiAction {
| 'other'; | 'other';
photo?: ApiPhoto; photo?: ApiPhoto;
amount?: number; amount?: number;
stars?: number;
transactionId?: string;
currency?: string; currency?: string;
giftCryptoInfo?: { giftCryptoInfo?: {
currency: string; currency: string;

View File

@ -142,8 +142,16 @@ export type ApiInputStorePaymentStarsTopup = {
amount: number; amount: number;
}; };
export type ApiInputStorePaymentStarsGift = {
type: 'starsgift';
user: ApiUser;
stars: number;
currency: string;
amount: number;
};
export type ApiInputStorePaymentPurpose = ApiInputStorePaymentGiveaway | ApiInputStorePaymentGiftcode | export type ApiInputStorePaymentPurpose = ApiInputStorePaymentGiveaway | ApiInputStorePaymentGiftcode |
ApiInputStorePaymentStarsTopup; ApiInputStorePaymentStarsTopup | ApiInputStorePaymentStarsGift;
export interface ApiPremiumGiftCodeOption { export interface ApiPremiumGiftCodeOption {
users: number; users: number;
@ -152,6 +160,13 @@ export interface ApiPremiumGiftCodeOption {
amount: number; amount: number;
} }
export interface ApiStarsGiftOption {
isExtended?: true;
stars: number;
currency: string;
amount: number;
}
export type ApiBoostsStatus = { export type ApiBoostsStatus = {
level: number; level: number;
currentLevelBoosts: number; currentLevelBoosts: number;

View File

@ -1273,3 +1273,6 @@
"ReplyInPrivateMessage" = "Reply In Private Message"; "ReplyInPrivateMessage" = "Reply In Private Message";
"AriaSearchOlderResult" = "Focus next result"; "AriaSearchOlderResult" = "Focus next result";
"AriaSearchNewerResult" = "Focus previous result"; "AriaSearchNewerResult" = "Focus previous result";
"CreditsBoxHistoryEntryGiftOutAbout" = "With Stars, {user} will be able to unlock content and services on Telegram. {link}"
"CreditsBoxOutAbout" = "Review the {link} for Stars."
"GiftStarsOutgoing" = "With Stars, {user} will be able to unlock content and services on Telegram."

View File

@ -16,13 +16,16 @@ export { default as BotTrustModal } from '../components/main/BotTrustModal';
export { default as AttachBotInstallModal } from '../components/modals/attachBotInstall/AttachBotInstallModal'; export { default as AttachBotInstallModal } from '../components/modals/attachBotInstall/AttachBotInstallModal';
export { default as DeleteFolderDialog } from '../components/main/DeleteFolderDialog'; export { default as DeleteFolderDialog } from '../components/main/DeleteFolderDialog';
export { default as PremiumMainModal } from '../components/main/premium/PremiumMainModal'; export { default as PremiumMainModal } from '../components/main/premium/PremiumMainModal';
export { default as GiftPremiumModal } from '../components/main/premium/GiftPremiumModal'; export { default as PremiumGiftModal } from '../components/main/premium/PremiumGiftModal';
export { default as StarsGiftModal } from '../components/main/premium/StarsGiftModal';
export { default as GiveawayModal } from '../components/main/premium/GiveawayModal'; export { default as GiveawayModal } from '../components/main/premium/GiveawayModal';
export { default as PremiumGiftingModal } from '../components/main/premium/PremiumGiftingModal'; export { default as PremiumGiftingPickerModal } from '../components/main/premium/PremiumGiftingPickerModal';
export { default as StarsGiftingPickerModal } from '../components/main/premium/StarsGiftingPickerModal';
export { default as PremiumLimitReachedModal } from '../components/main/premium/common/PremiumLimitReachedModal'; export { default as PremiumLimitReachedModal } from '../components/main/premium/common/PremiumLimitReachedModal';
export { default as StatusPickerMenu } from '../components/left/main/StatusPickerMenu'; export { default as StatusPickerMenu } from '../components/left/main/StatusPickerMenu';
export { default as BoostModal } from '../components/modals/boost/BoostModal'; export { default as BoostModal } from '../components/modals/boost/BoostModal';
export { default as GiftCodeModal } from '../components/modals/giftcode/GiftCodeModal'; export { default as GiftCodeModal } from '../components/modals/giftcode/GiftCodeModal';
export { default as StarGiftInfoModal } from '../components/modals/stars/StarGiftInfoModal';
export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal'; export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal';
export { default as StarsBalanceModal } from '../components/modals/stars/StarsBalanceModal'; export { default as StarsBalanceModal } from '../components/modals/stars/StarsBalanceModal';
export { default as StarPaymentModal } from '../components/modals/stars/StarsPaymentModal'; export { default as StarPaymentModal } from '../components/modals/stars/StarsPaymentModal';

View File

View File

@ -84,8 +84,41 @@ export function renderActionMessageText(
.replace('un1', '%action_origin%') .replace('un1', '%action_origin%')
.replace('%1$s', '%gift_payment_amount%'); .replace('%1$s', '%gift_payment_amount%');
} }
if (translationKey === 'ActionRequestedPeer') {
unprocessed = unprocessed
.replace('un1', '%star_target_user%')
.replace('un2', '%action_origin%')
.replace(/\*\*/g, '');
}
let processed: TextPart[]; let processed: TextPart[];
if (unprocessed.includes('%star_target_user%')) {
processed = processPlaceholder(
unprocessed,
'%star_target_user%',
targetUsers
? targetUsers.map((user) => renderUserContent(user, noLinks)).filter(Boolean)
: 'User',
);
unprocessed = processed.pop() as string;
content.push(...processed);
}
processed = processPlaceholder(
unprocessed,
'%action_origin%',
actionOriginChat ? (
renderChatContent(lang, actionOriginChat, noLinks) || NBSP
) : actionOriginUser ? (
renderUserContent(actionOriginUser, noLinks) || NBSP
) : 'User',
'',
);
unprocessed = processed.pop() as string;
content.push(...processed);
if (unprocessed.includes('%payment_amount%')) { if (unprocessed.includes('%payment_amount%')) {
processed = processPlaceholder( processed = processPlaceholder(
unprocessed, unprocessed,
@ -96,20 +129,6 @@ export function renderActionMessageText(
content.push(...processed); content.push(...processed);
} }
processed = processPlaceholder(
unprocessed,
'%action_origin%',
actionOriginUser ? (
renderUserContent(actionOriginUser, noLinks) || NBSP
) : actionOriginChat ? (
renderChatContent(lang, actionOriginChat, noLinks) || NBSP
) : 'User',
'',
);
unprocessed = processed.pop() as string;
content.push(...processed);
if (unprocessed.includes('%action_topic%')) { if (unprocessed.includes('%action_topic%')) {
const topicEmoji = topic?.iconEmojiId const topicEmoji = topic?.iconEmojiId
? <CustomEmoji documentId={topic.iconEmojiId} /> ? <CustomEmoji documentId={topic.iconEmojiId} />

View File

@ -14,6 +14,7 @@ type OwnProps = {
isConfirmDisabled?: boolean; isConfirmDisabled?: boolean;
shouldAdaptToSearch?: boolean; shouldAdaptToSearch?: boolean;
withFixedHeight?: boolean; withFixedHeight?: boolean;
withPremiumGradient?: boolean;
onConfirm?: NoneToVoidFunction; onConfirm?: NoneToVoidFunction;
} & ModalProps; } & ModalProps;
@ -23,6 +24,7 @@ const PickerModal = ({
shouldAdaptToSearch, shouldAdaptToSearch,
withFixedHeight, withFixedHeight,
onConfirm, onConfirm,
withPremiumGradient,
...modalProps ...modalProps
}: OwnProps) => { }: OwnProps) => {
const lang = useOldLang(); const lang = useOldLang();
@ -43,6 +45,7 @@ const PickerModal = ({
{modalProps.children} {modalProps.children}
<div className={styles.buttonWrapper}> <div className={styles.buttonWrapper}>
<Button <Button
withPremiumGradient={withPremiumGradient}
onClick={onConfirm || modalProps.onClose} onClick={onConfirm || modalProps.onClose}
color="primary" color="primary"
size="smaller" size="smaller"

View File

@ -53,7 +53,7 @@ const UserBirthday = ({
animatedEmojiEffects, animatedEmojiEffects,
isInSettings, isInSettings,
}: OwnProps & StateProps) => { }: OwnProps & StateProps) => {
const { openGiftPremiumModal, requestConfetti } = getActions(); const { openPremiumGiftModal, requestConfetti } = getActions();
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const animationPlayedRef = useRef(false); const animationPlayedRef = useRef(false);
@ -144,15 +144,15 @@ const UserBirthday = ({
const canGiftPremium = isToday && !user.isPremium && !user.isSelf && !isPremiumPurchaseBlocked; const canGiftPremium = isToday && !user.isPremium && !user.isSelf && !isPremiumPurchaseBlocked;
const handleOpenGiftModal = useLastCallback(() => { const handleOpenPremiumGiftModal = useLastCallback(() => {
openGiftPremiumModal({ forUserIds: [user.id] }); openPremiumGiftModal({ forUserIds: [user.id] });
}); });
const handleClick = useLastCallback(() => { const handleClick = useLastCallback(() => {
if (!isToday) return; if (!isToday) return;
if (canGiftPremium && animationPlayedRef.current) { if (canGiftPremium && animationPlayedRef.current) {
handleOpenGiftModal(); handleOpenPremiumGiftModal();
return; return;
} }
@ -173,7 +173,7 @@ const UserBirthday = ({
ripple={!isStatic} ripple={!isStatic}
onClick={handleClick} onClick={handleClick}
isStatic={isStatic} isStatic={isStatic}
onSecondaryIconClick={handleOpenGiftModal} onSecondaryIconClick={handleOpenPremiumGiftModal}
> >
<div className="title">{renderText(lang(valueKey, [formattedDate, age], undefined, age))}</div> <div className="title">{renderText(lang(valueKey, [formattedDate, age], undefined, age))}</div>
<span className="subtitle">{lang(isToday ? 'ProfileBirthdayToday' : 'ProfileBirthday')}</span> <span className="subtitle">{lang(isToday ? 'ProfileBirthdayToday' : 'ProfileBirthday')}</span>

View File

@ -59,7 +59,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
const [isSupportDialogOpen, openSupportDialog, closeSupportDialog] = useFlag(false); const [isSupportDialogOpen, openSupportDialog, closeSupportDialog] = useFlag(false);
const lang = useOldLang(); const oldLang = useOldLang();
useEffect(() => { useEffect(() => {
if (currentUserId) { if (currentUserId) {
@ -99,7 +99,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onClick={() => onScreenSelect(SettingsScreens.General)} onClick={() => onScreenSelect(SettingsScreens.General)}
> >
{lang('Telegram.GeneralSettingsViewController')} {oldLang('Telegram.GeneralSettingsViewController')}
</ListItem> </ListItem>
<ListItem <ListItem
icon="animations" icon="animations"
@ -107,7 +107,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onClick={() => onScreenSelect(SettingsScreens.Performance)} onClick={() => onScreenSelect(SettingsScreens.Performance)}
> >
{lang('Animations and Performance')} {oldLang('Animations and Performance')}
</ListItem> </ListItem>
<ListItem <ListItem
icon="unmute" icon="unmute"
@ -115,7 +115,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onClick={() => onScreenSelect(SettingsScreens.Notifications)} onClick={() => onScreenSelect(SettingsScreens.Notifications)}
> >
{lang('Notifications')} {oldLang('Notifications')}
</ListItem> </ListItem>
<ListItem <ListItem
icon="data" icon="data"
@ -123,7 +123,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onClick={() => onScreenSelect(SettingsScreens.DataStorage)} onClick={() => onScreenSelect(SettingsScreens.DataStorage)}
> >
{lang('DataSettings')} {oldLang('DataSettings')}
</ListItem> </ListItem>
<ListItem <ListItem
icon="lock" icon="lock"
@ -131,7 +131,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onClick={() => onScreenSelect(SettingsScreens.Privacy)} onClick={() => onScreenSelect(SettingsScreens.Privacy)}
> >
{lang('PrivacySettings')} {oldLang('PrivacySettings')}
</ListItem> </ListItem>
<ListItem <ListItem
icon="folder" icon="folder"
@ -139,7 +139,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onClick={() => onScreenSelect(SettingsScreens.Folders)} onClick={() => onScreenSelect(SettingsScreens.Folders)}
> >
{lang('Filters')} {oldLang('Filters')}
</ListItem> </ListItem>
<ListItem <ListItem
icon="active-sessions" icon="active-sessions"
@ -147,7 +147,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onClick={() => onScreenSelect(SettingsScreens.ActiveSessions)} onClick={() => onScreenSelect(SettingsScreens.ActiveSessions)}
> >
{lang('SessionsTitle')} {oldLang('SessionsTitle')}
{sessionCount > 0 && (<span className="settings-item__current-value">{sessionCount}</span>)} {sessionCount > 0 && (<span className="settings-item__current-value">{sessionCount}</span>)}
</ListItem> </ListItem>
<ListItem <ListItem
@ -156,8 +156,8 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onClick={() => onScreenSelect(SettingsScreens.Language)} onClick={() => onScreenSelect(SettingsScreens.Language)}
> >
{lang('Language')} {oldLang('Language')}
<span className="settings-item__current-value">{lang.langName}</span> <span className="settings-item__current-value">{oldLang.langName}</span>
</ListItem> </ListItem>
<ListItem <ListItem
icon="stickers" icon="stickers"
@ -165,7 +165,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onClick={() => onScreenSelect(SettingsScreens.Stickers)} onClick={() => onScreenSelect(SettingsScreens.Stickers)}
> >
{lang('StickersName')} {oldLang('StickersName')}
</ListItem> </ListItem>
</div> </div>
<div className="settings-main-menu"> <div className="settings-main-menu">
@ -176,7 +176,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onClick={() => openPremiumModal()} onClick={() => openPremiumModal()}
> >
{lang('TelegramPremium')} {oldLang('TelegramPremium')}
</ListItem> </ListItem>
)} )}
{shouldDisplayStars && ( {shouldDisplayStars && (
@ -186,7 +186,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onClick={() => openStarsBalanceModal({})} onClick={() => openStarsBalanceModal({})}
> >
{lang('MenuTelegramStars')} {oldLang('MenuTelegramStars')}
{Boolean(starsBalance) && ( {Boolean(starsBalance) && (
<span className="settings-item__current-value">{formatInteger(starsBalance)}</span> <span className="settings-item__current-value">{formatInteger(starsBalance)}</span>
)} )}
@ -199,7 +199,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onClick={() => openPremiumGiftingModal()} onClick={() => openPremiumGiftingModal()}
> >
{lang('GiftPremiumGifting')} {oldLang('GiftPremiumGifting')}
</ListItem> </ListItem>
)} )}
</div> </div>
@ -209,7 +209,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
narrow narrow
onClick={openSupportDialog} onClick={openSupportDialog}
> >
{lang('AskAQuestion')} {oldLang('AskAQuestion')}
</ListItem> </ListItem>
<ListItem <ListItem
icon="help" icon="help"
@ -217,7 +217,7 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onClick={() => openUrl({ url: FAQ_URL })} onClick={() => openUrl({ url: FAQ_URL })}
> >
{lang('TelegramFaq')} {oldLang('TelegramFaq')}
</ListItem> </ListItem>
<ListItem <ListItem
icon="privacy-policy" icon="privacy-policy"
@ -225,14 +225,14 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onClick={() => openUrl({ url: PRIVACY_URL })} onClick={() => openUrl({ url: PRIVACY_URL })}
> >
{lang('PrivacyPolicy')} {oldLang('PrivacyPolicy')}
</ListItem> </ListItem>
</div> </div>
<ConfirmDialog <ConfirmDialog
isOpen={isSupportDialogOpen} isOpen={isSupportDialogOpen}
confirmLabel={lang('lng_settings_ask_ok')} confirmLabel={oldLang('lng_settings_ask_ok')}
title={lang('AskAQuestion')} title={oldLang('AskAQuestion')}
text={lang('lng_settings_ask_sure')} text={oldLang('lng_settings_ask_sure')}
confirmHandler={handleOpenSupport} confirmHandler={handleOpenSupport}
onClose={closeSupportDialog} onClose={closeSupportDialog}
/> />

View File

@ -74,6 +74,7 @@ import ReactionPicker from '../middle/message/reactions/ReactionPicker.async';
import MessageListHistoryHandler from '../middle/MessageListHistoryHandler'; import MessageListHistoryHandler from '../middle/MessageListHistoryHandler';
import MiddleColumn from '../middle/MiddleColumn'; import MiddleColumn from '../middle/MiddleColumn';
import ModalContainer from '../modals/ModalContainer'; import ModalContainer from '../modals/ModalContainer';
import StarGiftInfoModal from '../modals/stars/StarGiftInfoModal';
import PaymentModal from '../payment/PaymentModal.async'; import PaymentModal from '../payment/PaymentModal.async';
import ReceiptModal from '../payment/ReceiptModal.async'; import ReceiptModal from '../payment/ReceiptModal.async';
import RightColumn from '../right/RightColumn'; import RightColumn from '../right/RightColumn';
@ -92,8 +93,9 @@ import NewContactModal from './NewContactModal.async';
import Notifications from './Notifications.async'; import Notifications from './Notifications.async';
import PremiumLimitReachedModal from './premium/common/PremiumLimitReachedModal.async'; import PremiumLimitReachedModal from './premium/common/PremiumLimitReachedModal.async';
import GiveawayModal from './premium/GiveawayModal.async'; import GiveawayModal from './premium/GiveawayModal.async';
import PremiumGiftingModal from './premium/PremiumGiftingModal.async'; import PremiumGiftingPickerModal from './premium/PremiumGiftingPickerModal.async';
import PremiumMainModal from './premium/PremiumMainModal.async'; import PremiumMainModal from './premium/PremiumMainModal.async';
import StarsGiftingPickerModal from './premium/StarsGiftingPickerModal.async';
import SafeLinkModal from './SafeLinkModal.async'; import SafeLinkModal from './SafeLinkModal.async';
import './Main.scss'; import './Main.scss';
@ -145,7 +147,9 @@ type StateProps = {
isAppendModalOpen?: boolean; isAppendModalOpen?: boolean;
isGiveawayModalOpen?: boolean; isGiveawayModalOpen?: boolean;
isDeleteMessageModalOpen?: boolean; isDeleteMessageModalOpen?: boolean;
isPremiumGiftingModalOpen?: boolean; isPremiumGiftingPickerModal?: boolean;
isStarsGiftingPickerModal?: boolean;
isStarGiftInfoModal?: boolean;
isCurrentUserPremium?: boolean; isCurrentUserPremium?: boolean;
noRightColumnAnimation?: boolean; noRightColumnAnimation?: boolean;
withInterfaceAnimations?: boolean; withInterfaceAnimations?: boolean;
@ -195,7 +199,9 @@ const Main: FC<OwnProps & StateProps> = ({
isPremiumModalOpen, isPremiumModalOpen,
isGiveawayModalOpen, isGiveawayModalOpen,
isDeleteMessageModalOpen, isDeleteMessageModalOpen,
isPremiumGiftingModalOpen, isPremiumGiftingPickerModal,
isStarsGiftingPickerModal,
isStarGiftInfoModal,
isPaymentModalOpen, isPaymentModalOpen,
isReceiptModalOpen, isReceiptModalOpen,
isReactionPickerOpen, isReactionPickerOpen,
@ -574,7 +580,9 @@ const Main: FC<OwnProps & StateProps> = ({
<MessageListHistoryHandler /> <MessageListHistoryHandler />
{isPremiumModalOpen && <PremiumMainModal isOpen={isPremiumModalOpen} />} {isPremiumModalOpen && <PremiumMainModal isOpen={isPremiumModalOpen} />}
{isGiveawayModalOpen && <GiveawayModal isOpen={isGiveawayModalOpen} />} {isGiveawayModalOpen && <GiveawayModal isOpen={isGiveawayModalOpen} />}
{isPremiumGiftingModalOpen && <PremiumGiftingModal isOpen={isPremiumGiftingModalOpen} />} {isPremiumGiftingPickerModal && <PremiumGiftingPickerModal isOpen={isPremiumGiftingPickerModal} />}
{isStarsGiftingPickerModal && <StarsGiftingPickerModal isOpen={isStarsGiftingPickerModal} />}
{isStarGiftInfoModal && <StarGiftInfoModal isOpen={isStarGiftInfoModal} />}
<PremiumLimitReachedModal limit={limitReached} /> <PremiumLimitReachedModal limit={limitReached} />
<PaymentModal isOpen={isPaymentModalOpen} onClose={closePaymentModal} /> <PaymentModal isOpen={isPaymentModalOpen} onClose={closePaymentModal} />
<ReceiptModal isOpen={isReceiptModalOpen} onClose={clearReceipt} /> <ReceiptModal isOpen={isReceiptModalOpen} onClose={clearReceipt} />
@ -616,6 +624,8 @@ export default memo(withGlobal<OwnProps>(
giveawayModal, giveawayModal,
deleteMessageModal, deleteMessageModal,
giftingModal, giftingModal,
starsGiftingModal,
starGiftInfoModal,
isMasterTab, isMasterTab,
payment, payment,
limitReachedModal, limitReachedModal,
@ -671,7 +681,9 @@ export default memo(withGlobal<OwnProps>(
isPremiumModalOpen: premiumModal?.isOpen, isPremiumModalOpen: premiumModal?.isOpen,
isGiveawayModalOpen: giveawayModal?.isOpen, isGiveawayModalOpen: giveawayModal?.isOpen,
isDeleteMessageModalOpen: Boolean(deleteMessageModal), isDeleteMessageModalOpen: Boolean(deleteMessageModal),
isPremiumGiftingModalOpen: giftingModal?.isOpen, isPremiumGiftingPickerModal: giftingModal?.isOpen,
isStarsGiftingPickerModal: starsGiftingModal?.isOpen,
isStarGiftInfoModal: starGiftInfoModal?.isOpen,
limitReached: limitReachedModal?.limit, limitReached: limitReachedModal?.limit,
isPaymentModalOpen: payment.isPaymentModalOpen, isPaymentModalOpen: payment.isPaymentModalOpen,
isReceiptModalOpen: Boolean(payment.receipt), isReceiptModalOpen: Boolean(payment.receipt),

View File

@ -1,46 +0,0 @@
@media (min-width: 451px) {
.modalDialog :global(.modal-dialog) {
max-width: 32rem !important;
}
}
.closeButton {
position: absolute;
top: 0.5rem;
left: 0.5rem;
}
.avatars {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 1rem;
margin: 1rem;
}
.center {
text-align: center;
}
.description,
.premiumFeatures {
text-align: center;
margin: 0 auto 2rem;
max-width: 25rem;
}
.premiumFeatures {
font-size: 0.9375rem;
color: var(--color-text-secondary);
}
.options {
margin-bottom: 2.5rem;
}
.boostIcon {
color: var(--color-primary);
vertical-align: middle;
line-height: 1.5;
}

View File

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

View File

@ -0,0 +1,123 @@
@use '../../../styles/mixins';
@media (min-width: 451px) {
.modalDialog :global(.modal-dialog) {
max-width: 32rem !important;
}
}
.root {
z-index: calc(var(--z-media-viewer) - 1);
}
.root :global(.modal-content) {
padding: 0;
}
.root :global(.modal-dialog) {
height: min(calc(55vh + 41px + 193px), 90vh);
max-width: 26.25rem;
}
.root :global(.modal-dialog),
.root :global(.modal-content),
.transition {
overflow: hidden;
}
.content {
display: flex;
flex-direction: column;
}
.main {
overflow-y: scroll;
}
.giftSection {
padding: 1.5rem;
}
.section {
padding: 0.5rem;
}
.header {
z-index: 2;
display: flex;
align-items: center;
border-bottom: 0.0625rem solid var(--color-borders);
position: absolute;
width: 100%;
left: 0;
top: 0;
height: 3.5rem;
padding: 0.5rem;
background: var(--color-background);
transition: 0.25s ease-out transform;
}
.starHeaderText {
font-size: 1.25rem;
font-weight: 500;
margin: 0 0 0 3rem;
unicode-bidi: plaintext;
}
.hiddenHeader {
transform: translateY(-100%);
}
.closeButton {
position: absolute;
top: 0.5rem;
left: 0.5rem;
z-index: 3;
}
.avatars {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 1rem;
margin: 1rem;
}
.center {
text-align: center;
}
.description,
.premiumFeatures {
text-align: center;
margin: 0 auto 2rem;
max-width: 25rem;
}
.premiumFeatures {
font-size: 0.9375rem;
color: var(--color-text-secondary);
}
.boostIcon {
color: var(--color-primary);
vertical-align: middle;
line-height: 1.5;
}
.optionBottom {
font-size: 0.875rem;
color: var(--color-text-secondary);
}
.secondaryInfo {
text-align: center;
font-size: 0.875rem;
color: var(--color-text-secondary);
padding: 0.5rem 1rem;
}
.footer {
margin: 0 1.5rem;
}

View File

@ -28,7 +28,7 @@ import Link from '../../ui/Link';
import Modal from '../../ui/Modal'; import Modal from '../../ui/Modal';
import PremiumSubscriptionOption from './PremiumSubscriptionOption'; import PremiumSubscriptionOption from './PremiumSubscriptionOption';
import styles from './GiftPremiumModal.module.scss'; import styles from './PremiumGiftModal.module.scss';
export type OwnProps = { export type OwnProps = {
isOpen?: boolean; isOpen?: boolean;
@ -41,7 +41,7 @@ type StateProps = {
boostPerSentGift?: number; boostPerSentGift?: number;
}; };
const GiftPremiumModal: FC<OwnProps & StateProps> = ({ const PremiumGiftModal: FC<OwnProps & StateProps> = ({
isOpen, isOpen,
isCompleted, isCompleted,
gifts, gifts,
@ -52,10 +52,11 @@ const GiftPremiumModal: FC<OwnProps & StateProps> = ({
const dialogRef = useRef<HTMLDivElement>(null); const dialogRef = useRef<HTMLDivElement>(null);
const { const {
openPremiumModal, closeGiftPremiumModal, openInvoice, requestConfetti, openPremiumModal, closePremiumGiftModal, openInvoice, requestConfetti,
} = getActions(); } = getActions();
const lang = useOldLang(); const oldLang = useOldLang();
const [selectedMonthOption, setSelectedMonthOption] = useState<number | undefined>(); const [selectedMonthOption, setSelectedMonthOption] = useState<number | undefined>();
const selectedUserQuantity = forUserIds && forUserIds.length * boostPerSentGift; const selectedUserQuantity = forUserIds && forUserIds.length * boostPerSentGift;
@ -140,23 +141,23 @@ const GiftPremiumModal: FC<OwnProps & StateProps> = ({
function renderGiftTitle() { function renderGiftTitle() {
if (isCompleted) { if (isCompleted) {
return renderText(lang('TelegramPremiumUserGiftedPremiumOutboundDialogTitle', return renderText(oldLang('TelegramPremiumUserGiftedPremiumOutboundDialogTitle',
[userNameList, selectedGift?.months]), ['simple_markdown']); [userNameList, selectedGift?.months]), ['simple_markdown']);
} }
return lang('GiftTelegramPremiumTitle'); return oldLang('GiftTelegramPremiumTitle');
} }
function renderGiftText() { function renderGiftText() {
if (isCompleted) { if (isCompleted) {
return renderText(lang('TelegramPremiumUserGiftedPremiumOutboundDialogSubtitle', userNameList), return renderText(oldLang('TelegramPremiumUserGiftedPremiumOutboundDialogSubtitle', userNameList),
['simple_markdown']); ['simple_markdown']);
} }
return renderText(lang('GiftPremiumUsersGiveAccessManyZero', userNameList), ['simple_markdown']); return renderText(oldLang('GiftPremiumUsersGiveAccessManyZero', userNameList), ['simple_markdown']);
} }
function renderPremiumFeaturesLink() { function renderPremiumFeaturesLink() {
const info = lang('GiftPremiumListFeaturesAndTerms'); const info = oldLang('GiftPremiumListFeaturesAndTerms');
// Translation hack for rendering component inside string // Translation hack for rendering component inside string
const parts = info.match(/([^*]*)\*([^*]+)\*(.*)/); const parts = info.match(/([^*]*)\*([^*]+)\*(.*)/);
@ -174,7 +175,8 @@ const GiftPremiumModal: FC<OwnProps & StateProps> = ({
} }
function renderBoostsPluralText() { function renderBoostsPluralText() {
const giftParts = renderText(lang('GiftPremiumWillReceiveBoostsPlural', selectedUserQuantity), ['simple_markdown']); const giftParts = renderText(oldLang('GiftPremiumWillReceiveBoostsPlural',
selectedUserQuantity), ['simple_markdown']);
return giftParts.map((part) => { return giftParts.map((part) => {
if (typeof part === 'string') { if (typeof part === 'string') {
return part.split(/(⚡)/g).map((subpart) => { return part.split(/(⚡)/g).map((subpart) => {
@ -210,19 +212,20 @@ const GiftPremiumModal: FC<OwnProps & StateProps> = ({
return ( return (
<Modal <Modal
dialogRef={dialogRef} dialogRef={dialogRef}
onClose={closeGiftPremiumModal} onClose={closePremiumGiftModal}
isOpen={isOpen} isOpen={isOpen}
className={styles.modalDialog} contentClassName={styles.content}
className={buildClassName(styles.modalDialog, styles.root)}
> >
<div className="custom-scroll"> <div className={buildClassName(styles.main, 'custom-scroll')}>
<Button <Button
round round
size="smaller" size="smaller"
className={styles.closeButton} className={styles.closeButton}
color="translucent" color="translucent"
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
onClick={() => closeGiftPremiumModal()} onClick={() => closePremiumGiftModal()}
ariaLabel={lang('Close')} ariaLabel={oldLang('Close')}
> >
<i className="icon icon-close" /> <i className="icon icon-close" />
</Button> </Button>
@ -244,7 +247,7 @@ const GiftPremiumModal: FC<OwnProps & StateProps> = ({
{renderText(renderBoostsPluralText(), ['simple_markdown', 'emoji'])} {renderText(renderBoostsPluralText(), ['simple_markdown', 'emoji'])}
</p> </p>
<div className={styles.options}> <div className={styles.giftSection}>
{renderSubscriptionGiftOptions()} {renderSubscriptionGiftOptions()}
</div> </div>
</> </>
@ -253,12 +256,14 @@ const GiftPremiumModal: FC<OwnProps & StateProps> = ({
</div> </div>
{!isCompleted && ( {!isCompleted && (
<Button withPremiumGradient className={styles.button} isShiny disabled={!selectedGift} onClick={handleSubmit}> <div className={styles.footer}>
{lang( <Button withPremiumGradient isShiny disabled={!selectedGift} onClick={handleSubmit}>
'GiftSubscriptionFor', selectedGift {oldLang(
&& formatCurrency(selectedGift!.amount, selectedGift.currency, lang.code), 'GiftSubscriptionFor', selectedGift
)} && formatCurrency(selectedGift!.amount, selectedGift.currency, oldLang.code),
</Button> )}
</Button>
</div>
)} )}
</Modal> </Modal>
); );
@ -267,7 +272,7 @@ const GiftPremiumModal: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>((global): StateProps => { export default memo(withGlobal<OwnProps>((global): StateProps => {
const { const {
gifts, forUserIds, isCompleted, gifts, forUserIds, isCompleted,
} = selectTabState(global).giftPremiumModal || {}; } = selectTabState(global).giftModal || {};
return { return {
isCompleted, isCompleted,
@ -275,4 +280,4 @@ export default memo(withGlobal<OwnProps>((global): StateProps => {
boostPerSentGift: global.appConfig?.boostsPerSentGift, boostPerSentGift: global.appConfig?.boostsPerSentGift,
forUserIds, forUserIds,
}; };
})(GiftPremiumModal)); })(PremiumGiftModal));

View File

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

View File

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

View File

@ -8,19 +8,16 @@ import { GIVEAWAY_MAX_ADDITIONAL_CHANNELS } from '../../../config';
import { import {
filterUsersByName, isUserBot, filterUsersByName, isUserBot,
} from '../../../global/helpers'; } from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName';
import { unique } from '../../../util/iteratees'; import { unique } from '../../../util/iteratees';
import sortChatIds from '../../common/helpers/sortChatIds'; import sortChatIds from '../../common/helpers/sortChatIds';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import Icon from '../../common/icons/Icon';
import PeerPicker from '../../common/pickers/PeerPicker'; import PeerPicker from '../../common/pickers/PeerPicker';
import Button from '../../ui/Button'; import PickerModal from '../../common/pickers/PickerModal';
import Modal from '../../ui/Modal';
import styles from './PremiumGiftingModal.module.scss'; import styles from './PremiumGiftingPickerModal.module.scss';
export type OwnProps = { export type OwnProps = {
isOpen?: boolean; isOpen?: boolean;
@ -32,15 +29,15 @@ interface StateProps {
userIds?: string[]; userIds?: string[];
} }
const PremiumGiftingModal: FC<OwnProps & StateProps> = ({ const PremiumGiftingPickerModal: FC<OwnProps & StateProps> = ({
isOpen, isOpen,
currentUserId, currentUserId,
userSelectionLimit = GIVEAWAY_MAX_ADDITIONAL_CHANNELS, userSelectionLimit = GIVEAWAY_MAX_ADDITIONAL_CHANNELS,
userIds, userIds,
}) => { }) => {
const { closePremiumGiftingModal, openGiftPremiumModal, showNotification } = getActions(); const { closePremiumGiftingModal, openPremiumGiftModal, showNotification } = getActions();
const lang = useOldLang(); const oldLang = useOldLang();
const [selectedUserIds, setSelectedUserIds] = useState<string[]>([]); const [selectedUserIds, setSelectedUserIds] = useState<string[]>([]);
const [searchQuery, setSearchQuery] = useState<string>(''); const [searchQuery, setSearchQuery] = useState<string>('');
@ -61,8 +58,7 @@ const PremiumGiftingModal: FC<OwnProps & StateProps> = ({
const handleSendIdList = useLastCallback(() => { const handleSendIdList = useLastCallback(() => {
if (selectedUserIds?.length) { if (selectedUserIds?.length) {
openGiftPremiumModal({ forUserIds: selectedUserIds }); openPremiumGiftModal({ forUserIds: selectedUserIds });
closePremiumGiftingModal(); closePremiumGiftingModal();
} }
}); });
@ -70,65 +66,42 @@ const PremiumGiftingModal: FC<OwnProps & StateProps> = ({
const handleSelectedUserIdsChange = useLastCallback((newSelectedIds: string[]) => { const handleSelectedUserIdsChange = useLastCallback((newSelectedIds: string[]) => {
if (newSelectedIds.length > userSelectionLimit) { if (newSelectedIds.length > userSelectionLimit) {
showNotification({ showNotification({
message: lang('BoostingSelectUpToWarningUsers', userSelectionLimit), message: oldLang('BoostingSelectUpToWarningUsers', userSelectionLimit),
}); });
return; return;
} }
setSelectedUserIds(newSelectedIds); setSelectedUserIds(newSelectedIds);
}); });
function renderSearchField() {
return (
<div className={styles.filter} dir={lang.isRtl ? 'rtl' : undefined}>
<Button
round
size="smaller"
color="translucent"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => closePremiumGiftingModal()}
ariaLabel={lang('Close')}
>
<Icon name="close" />
</Button>
<h3 className={styles.title}>{lang('GiftTelegramPremiumTitle')}
</h3>
</div>
);
}
return ( return (
<Modal <PickerModal
className={styles.root} className={styles.root}
isOpen={isOpen} isOpen={isOpen}
onClose={closePremiumGiftingModal} onClose={closePremiumGiftingModal}
title={oldLang('GiftTelegramPremiumTitle')}
hasCloseButton
shouldAdaptToSearch
withFixedHeight
confirmButtonText={oldLang('Continue')}
onConfirm={handleSendIdList}
onEnter={handleSendIdList} onEnter={handleSendIdList}
withPremiumGradient
> >
<div className={styles.main}> <PeerPicker
{renderSearchField()} className={styles.picker}
<div className={buildClassName(styles.main, 'custom-scroll')}> itemIds={displayedUserIds}
<PeerPicker selectedIds={selectedUserIds}
className={styles.picker} filterValue={searchQuery}
itemIds={displayedUserIds} filterPlaceholder={oldLang('Search')}
selectedIds={selectedUserIds} onSelectedIdsChange={handleSelectedUserIdsChange}
filterValue={searchQuery} onFilterChange={setSearchQuery}
filterPlaceholder={lang('Search')} isSearchable
searchInputId="users-picker-search" withDefaultPadding
onSelectedIdsChange={handleSelectedUserIdsChange} withStatus
onFilterChange={setSearchQuery} allowMultiple
isSearchable itemInputType="checkbox"
withDefaultPadding />
withStatus </PickerModal>
allowMultiple
itemInputType="checkbox"
/>
</div>
<div className={styles.buttons}>
<Button withPremiumGradient size="smaller" onClick={handleSendIdList} disabled={!selectedUserIds?.length}>
{lang('Continue')}
</Button>
</div>
</div>
</Modal>
); );
}; };
@ -140,4 +113,4 @@ export default memo(withGlobal<OwnProps>((global): StateProps => {
userIds: global.contactList?.userIds, userIds: global.contactList?.userIds,
userSelectionLimit: global.appConfig?.giveawayAddPeersMax, userSelectionLimit: global.appConfig?.giveawayAddPeersMax,
}; };
})(PremiumGiftingModal)); })(PremiumGiftingPickerModal));

View File

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

View File

@ -0,0 +1,107 @@
@media (min-width: 451px) {
.modalDialog :global(.modal-dialog) {
max-width: 32rem !important;
}
}
.root {
z-index: calc(var(--z-media-viewer) - 1);
}
.root :global(.modal-content) {
padding: 0;
}
.root :global(.modal-dialog) {
height: min(calc(55vh + 41px + 193px), 90vh);
max-width: 26.25rem;
}
.root :global(.modal-dialog),
.root :global(.modal-content),
.transition {
overflow: hidden;
}
.main {
height: 100%;
overflow-y: scroll;
display: flex;
flex-direction: column;
}
.section {
padding: 0.5rem;
}
.header {
z-index: 2;
display: flex;
align-items: center;
border-bottom: 0.0625rem solid var(--color-borders);
position: absolute;
width: 100%;
left: 0;
top: 0;
height: 3.5rem;
padding: 0.5rem;
background: var(--color-background);
transition: 0.25s ease-out transform;
}
.starHeaderText {
font-size: 1.25rem;
font-weight: 500;
margin: 0 0 0 3.5rem;
unicode-bidi: plaintext;
}
.hiddenHeader {
transform: translateY(-100%);
}
.closeButton {
position: absolute;
top: 0.5rem;
left: 0.5rem;
z-index: 3;
}
.avatars {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 1rem;
margin: 1rem;
}
.center {
text-align: center;
}
.options {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.5rem;
width: 100%;
margin-bottom: 2.5rem;
}
.boostIcon {
color: var(--color-primary);
vertical-align: middle;
line-height: 1.5;
}
.moreOptions {
grid-column: 1/-1;
}
.secondaryInfo {
text-align: center;
font-size: 0.875rem;
color: var(--color-text-secondary);
padding: 0.5rem 1rem;
margin-top: auto;
}

View File

@ -0,0 +1,197 @@
import type { FC } from '../../../lib/teact/teact';
import React, {
memo, useEffect, useMemo, useRef,
useState,
} from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global';
import type {
ApiStarsGiftOption, ApiStarTopupOption, ApiUser,
} from '../../../api/types';
import {
selectTabState, selectUser,
} from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { formatCurrencyAsString } from '../../../util/formatCurrency';
import renderText from '../../common/helpers/renderText';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import Avatar from '../../common/Avatar';
import SafeLink from '../../common/SafeLink';
import StarTopupOptionList from '../../modals/stars/StarTopupOptionList';
import Button from '../../ui/Button';
import Modal from '../../ui/Modal';
import styles from './StarsGiftModal.module.scss';
export type OwnProps = {
isOpen?: boolean;
};
type StateProps = {
isCompleted?: boolean;
starsGiftOptions?: ApiStarsGiftOption[] | undefined;
forUserId?: string;
user?: ApiUser;
};
const StarsGiftModal: FC<OwnProps & StateProps> = ({
isOpen,
isCompleted,
starsGiftOptions,
forUserId,
user,
}) => {
// eslint-disable-next-line no-null/no-null
const dialogRef = useRef<HTMLDivElement>(null);
const {
closeStarsGiftModal, openInvoice, requestConfetti,
} = getActions();
const oldLang = useOldLang();
const [selectedOption, setSelectedOption] = useState<ApiStarTopupOption | undefined>();
const [isHeaderHidden, setHeaderHidden] = useState(true);
useEffect(() => {
if (!isOpen) {
setHeaderHidden(true);
}
}, [isOpen]);
const showConfetti = useLastCallback(() => {
const dialog = dialogRef.current;
if (!dialog) return;
if (isOpen) {
const {
top, left, width, height,
} = dialog.querySelector('.modal-content')!.getBoundingClientRect();
requestConfetti({
top,
left,
width,
height,
withStars: true,
});
}
});
useEffect(() => {
if (isCompleted) {
showConfetti();
}
}, [isCompleted, showConfetti]);
const handleClick = useLastCallback((option: ApiStarTopupOption) => {
setSelectedOption(option);
openInvoice({
type: 'starsgift',
userId: forUserId!,
stars: option.stars,
currency: option.currency,
amount: option.amount,
});
});
function handleScroll(e: React.UIEvent<HTMLDivElement>) {
const { scrollTop } = e.currentTarget;
setHeaderHidden(scrollTop <= 150);
}
function renderGiftTitle() {
if (isCompleted) {
return renderText(oldLang('Notification.StarsGift.SentYou',
formatCurrencyAsString(selectedOption!.amount, selectedOption!.currency, oldLang.code)), ['simple_markdown']);
}
return oldLang('GiftStarsTitle');
}
function renderStarOptionList() {
return (
<StarTopupOptionList
options={starsGiftOptions}
onClick={handleClick}
/>
);
}
const bottomText = useMemo(() => {
if (!isOpen) return undefined;
const text = oldLang('lng_credits_summary_options_about');
const parts = text.split('{link}');
return [
parts[0],
<SafeLink url={oldLang('StarsTOSLink')} text={oldLang('lng_credits_summary_options_about_link')} />,
parts[1],
];
}, [isOpen, oldLang]);
return (
<Modal
dialogRef={dialogRef}
onClose={closeStarsGiftModal}
isOpen={isOpen}
className={buildClassName(styles.modalDialog, styles.root)}
>
<div className={buildClassName(styles.main, 'custom-scroll')} onScroll={handleScroll}>
<Button
round
size="smaller"
className={styles.closeButton}
color="translucent"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => closeStarsGiftModal()}
ariaLabel={oldLang('Close')}
>
<i className="icon icon-close" />
</Button>
<div className={buildClassName(styles.header, isHeaderHidden && styles.hiddenHeader)}>
<h2 className={styles.starHeaderText}>
{oldLang('GiftStarsTitle')}
</h2>
</div>
<div className={styles.avatars}>
<Avatar
size="large"
peer={user}
/>
</div>
<h2 className={buildClassName(styles.headerText, styles.center)}>
{renderGiftTitle()}
</h2>
{!isCompleted && (
<>
<div className={buildClassName(styles.section, styles.options)}>
{renderStarOptionList()}
</div>
<div className={styles.secondaryInfo}>
{bottomText}
</div>
</>
)}
</div>
</Modal>
);
};
export default memo(withGlobal<OwnProps>((global): StateProps => {
const {
starsGiftOptions, forUserId, isCompleted,
} = selectTabState(global).starsGiftModal || {};
const user = forUserId ? selectUser(getGlobal(), forUserId) : undefined;
return {
isCompleted,
starsGiftOptions,
forUserId,
user,
};
})(StarsGiftModal));

View File

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

View File

@ -0,0 +1,86 @@
.root {
z-index: calc(var(--z-media-viewer) - 1);
}
.root :global(.modal-content) {
padding: 0;
}
.root :global(.modal-dialog) {
max-width: 55vh;
}
.root :global(.modal-dialog),
.root :global(.modal-content),
.transition {
overflow: hidden;
}
.main {
height: 90vh;
}
.filter {
padding: 0.375rem 1rem 0.25rem 0.75rem;
margin-bottom: 0.625rem;
background-color: var(--color-background);
box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent);
border-bottom: 0.625rem solid var(--color-background-secondary);
display: flex;
flex-flow: row wrap;
align-items: center;
flex-shrink: 0;
overflow-y: auto;
max-height: 20rem;
}
.title {
margin: 0;
}
.buttons {
width: 100%;
background: var(--color-background);
position: absolute;
bottom: 0;
z-index: 1;
padding: 0.75rem;
}
.picker {
height: 75vh;
}
.avatars {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 1rem;
margin: 1rem;
}
.center {
text-align: center;
}
.description,
.premiumFeatures {
text-align: center;
margin: 0 auto 2rem;
max-width: 25rem;
}
.premiumFeatures {
font-size: 0.9375rem;
color: var(--color-text-secondary);
}
.options {
margin-bottom: 2.5rem;
}
.button {
height: 3rem;
font-weight: 600;
}

View File

@ -0,0 +1,141 @@
import type { FC } from '../../../lib/teact/teact';
import React, {
memo, useMemo,
useRef,
useState,
} from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global';
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
import {
filterUsersByName, isDeletedUser, isUserBot,
} from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName';
import { unique } from '../../../util/iteratees';
import sortChatIds from '../../common/helpers/sortChatIds';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import Icon from '../../common/icons/Icon';
import PeerPicker from '../../common/pickers/PeerPicker';
import Button from '../../ui/Button';
import Modal from '../../ui/Modal';
import styles from './StarsGiftingPickerModal.module.scss';
export type OwnProps = {
isOpen?: boolean;
};
interface StateProps {
currentUserId?: string;
userIds?: string[];
activeListIds?: string[];
archivedListIds?: string[];
}
const StarsGiftingPickerModal: FC<OwnProps & StateProps> = ({
isOpen,
currentUserId,
activeListIds,
archivedListIds,
userIds,
}) => {
// eslint-disable-next-line no-null/no-null
const dialogRef = useRef<HTMLDivElement>(null);
const { closeStarsGiftingModal, openStarsGiftModal } = getActions();
const oldLang = useOldLang();
const [searchQuery, setSearchQuery] = useState<string>('');
const displayedUserIds = useMemo(() => {
const usersById = getGlobal().users.byId;
const combinedIds = [
...(userIds || []),
...(activeListIds || []),
...(archivedListIds || []),
];
const filteredContactIds = filterUsersByName(combinedIds, usersById, searchQuery);
return sortChatIds(unique(filteredContactIds).filter((id) => {
const user = usersById[id];
if (!user) {
return false;
}
return !user.isSupport
&& !isUserBot(user) && !isDeletedUser(user)
&& id !== currentUserId && id !== SERVICE_NOTIFICATIONS_USER_ID;
}));
}, [currentUserId, searchQuery, userIds, activeListIds, archivedListIds]);
const handleSelectedUserIdsChange = useLastCallback((newSelectedId?: string) => {
if (newSelectedId?.length) {
openStarsGiftModal({ forUserId: newSelectedId });
}
});
function renderHeaderText() {
return (
<div className={styles.filter} dir={oldLang.isRtl ? 'rtl' : undefined}>
<Button
round
size="smaller"
color="translucent"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => closeStarsGiftingModal()}
ariaLabel={oldLang('Close')}
>
<Icon name="close" />
</Button>
<h3 className={buildClassName(styles.title, 'ml-2')}>{oldLang('GiftStarsTitle')}
</h3>
</div>
);
}
return (
<Modal
className={styles.root}
isOpen={isOpen}
onClose={closeStarsGiftingModal}
onEnter={handleSelectedUserIdsChange}
dialogRef={dialogRef}
>
<div className={buildClassName(styles.main, 'custom-scroll')}>
{renderHeaderText()}
<PeerPicker
className={styles.picker}
itemIds={displayedUserIds}
filterValue={searchQuery}
filterPlaceholder={oldLang('Search')}
onFilterChange={setSearchQuery}
isSearchable
withDefaultPadding
withStatus
onSelectedIdChange={handleSelectedUserIdsChange}
/>
</div>
</Modal>
);
};
export default memo(withGlobal<OwnProps>((global): StateProps => {
const {
chats: {
listIds,
},
currentUserId,
} = global;
return {
userIds: global.contactList?.userIds,
activeListIds: listIds.active,
archivedListIds: listIds.archived,
currentUserId,
};
})(StarsGiftingPickerModal));

View File

@ -13,7 +13,7 @@ import type { FocusDirection, ThreadId } from '../../types';
import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage'; import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage';
import { import {
getChatTitle, getMessageHtmlId, isJoinedChannelMessage, getChatTitle, getMessageHtmlId, getSenderTitle, isJoinedChannelMessage,
} from '../../global/helpers'; } from '../../global/helpers';
import { getMessageReplyInfo } from '../../global/helpers/replies'; import { getMessageReplyInfo } from '../../global/helpers/replies';
import { import {
@ -35,6 +35,7 @@ import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import useEnsureMessage from '../../hooks/useEnsureMessage'; import useEnsureMessage from '../../hooks/useEnsureMessage';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
import { useIsIntersecting, useOnIntersect } from '../../hooks/useIntersectionObserver'; import { useIsIntersecting, useOnIntersect } from '../../hooks/useIntersectionObserver';
import useLang from '../../hooks/useLang';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import useShowTransition from '../../hooks/useShowTransition'; import useShowTransition from '../../hooks/useShowTransition';
import useFocusMessage from './message/hooks/useFocusMessage'; import useFocusMessage from './message/hooks/useFocusMessage';
@ -103,10 +104,11 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
onPinnedIntersectionChange, onPinnedIntersectionChange,
}) => { }) => {
const { const {
openPremiumModal, requestConfetti, checkGiftCode, getReceipt, openPremiumModal, requestConfetti, checkGiftCode, getReceipt, openStarGiftInfoModal,
} = getActions(); } = getActions();
const lang = useOldLang(); const oldLang = useOldLang();
const lang = useLang();
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
@ -133,6 +135,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
const isGiftCode = Boolean(message.content.action?.text.startsWith('BoostingReceivedGift')); const isGiftCode = Boolean(message.content.action?.text.startsWith('BoostingReceivedGift'));
const isSuggestedAvatar = message.content.action?.type === 'suggestProfilePhoto' && message.content.action!.photo; const isSuggestedAvatar = message.content.action?.type === 'suggestProfilePhoto' && message.content.action!.photo;
const isJoinedMessage = isJoinedChannelMessage(message); const isJoinedMessage = isJoinedChannelMessage(message);
const hasStars = Boolean(message.content.action?.stars);
useEffect(() => { useEffect(() => {
if (noAppearanceAnimation) { if (noAppearanceAnimation) {
@ -168,7 +171,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
const renderContent = useCallback(() => { const renderContent = useCallback(() => {
return renderActionMessageText( return renderActionMessageText(
lang, oldLang,
message, message,
senderUser, senderUser,
senderChat, senderChat,
@ -181,7 +184,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
observeIntersectionForPlaying, observeIntersectionForPlaying,
); );
}, [ }, [
isEmbedded, lang, message, observeIntersectionForLoading, observeIntersectionForPlaying, isEmbedded, message, observeIntersectionForLoading, observeIntersectionForPlaying, oldLang,
senderChat, senderUser, targetChatId, targetMessage, targetUsers, topic, senderChat, senderUser, targetChatId, targetMessage, targetUsers, topic,
]); ]);
@ -197,6 +200,14 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
handleBeforeContextMenu(e); handleBeforeContextMenu(e);
}; };
const handleStarGiftClick = () => {
openStarGiftInfoModal({
toUserId: targetUserIds?.[0],
stars: message.content.action!.stars,
date: message.date,
});
};
const handlePremiumGiftClick = () => { const handlePremiumGiftClick = () => {
openPremiumModal({ openPremiumModal({
isGift: true, isGift: true,
@ -233,7 +244,12 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
function renderGift() { function renderGift() {
return ( return (
<span className="action-message-gift" tabIndex={0} role="button" onClick={handlePremiumGiftClick}> <span
className="action-message-gift action-message-stars-gift"
tabIndex={0}
role="button"
onClick={hasStars ? handleStarGiftClick : handlePremiumGiftClick}
>
<AnimatedIconFromSticker <AnimatedIconFromSticker
key={message.id} key={message.id}
sticker={premiumGiftSticker} sticker={premiumGiftSticker}
@ -241,10 +257,14 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
noLoop noLoop
nonInteractive nonInteractive
/> />
<strong>{lang('ActionGiftPremiumTitle')}</strong> <strong>{hasStars ? oldLang('Stars', message.content.action?.stars)
<span>{lang('ActionGiftPremiumSubtitle', lang('Months', message.content.action?.months, 'i'))}</span> : oldLang('ActionGiftPremiumTitle')}
</strong>
<span>{hasStars ? oldLang('ActionGiftStarsSubtitleYou')
: oldLang('ActionGiftPremiumSubtitle', oldLang('Months', message.content.action?.months, 'i'))}
</span>
<span className="action-message-button">{lang('ActionGiftPremiumView')}</span> <span className="action-message-button">{oldLang('ActionGiftPremiumView')}</span>
</span> </span>
); );
} }
@ -257,7 +277,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
className="action-message-gift action-message-gift-code" className="action-message-gift action-message-gift-code"
tabIndex={0} tabIndex={0}
role="button" role="button"
onClick={handleGiftCodeClick} onClick={hasStars ? handleStarGiftClick : handleGiftCodeClick}
> >
<AnimatedIconFromSticker <AnimatedIconFromSticker
key={message.id} key={message.id}
@ -266,21 +286,36 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
noLoop noLoop
nonInteractive nonInteractive
/> />
<strong>{lang(isUnclaimed ? 'BoostingUnclaimedPrize' : 'BoostingCongratulations')}</strong> <strong>{hasStars ? oldLang('Stars', message.content.action?.stars)
: oldLang(isUnclaimed ? 'BoostingUnclaimedPrize' : 'BoostingCongratulations')}
</strong>
<span className="action-message-subtitle"> <span className="action-message-subtitle">
{targetChat && renderText(lang(isFromGiveaway ? 'BoostingReceivedGiftFrom' : isUnclaimed {hasStars ? lang('GiftStarsOutgoing', {
user: (
<b>
{senderUser && renderText(getSenderTitle(oldLang, senderUser) || '', ['simple_markdown'])}
</b>
),
}, {
withNodes: true,
}) : targetChat && renderText(oldLang(isFromGiveaway ? 'BoostingReceivedGiftFrom' : isUnclaimed
? 'BoostingReceivedPrizeFrom' : 'BoostingYouHaveUnclaimedPrize', ? 'BoostingReceivedPrizeFrom' : 'BoostingYouHaveUnclaimedPrize',
getChatTitle(lang, targetChat)), getChatTitle(oldLang, targetChat)),
['simple_markdown'])} ['simple_markdown'])}
</span> </span>
<span className="action-message-subtitle"> {!hasStars && (
{renderText(lang( <span className="action-message-subtitle">
'BoostingUnclaimedPrizeDuration', {renderText(oldLang(
lang('Months', message.content.action?.months, 'i'), 'BoostingUnclaimedPrizeDuration',
), ['simple_markdown'])} oldLang('Months', message.content.action?.months, 'i'),
</span> ), ['simple_markdown'])}
</span>
)}
<span className="action-message-button">{lang('BoostingReceivedGiftOpenBtn')}</span> <span className="action-message-button">{
oldLang(hasStars ? 'ActionGiftPremiumView' : 'BoostingReceivedGiftOpenBtn')
}
</span>
</span> </span>
); );
} }

View File

@ -187,7 +187,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
requestMasterAndRequestCall, requestMasterAndRequestCall,
toggleStatistics, toggleStatistics,
openBoostStatistics, openBoostStatistics,
openGiftPremiumModal, openPremiumGiftModal,
openThreadWithInfo, openThreadWithInfo,
openCreateTopicPanel, openCreateTopicPanel,
openEditTopicPanel, openEditTopicPanel,
@ -313,7 +313,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
}); });
const handleGiftPremiumClick = useLastCallback(() => { const handleGiftPremiumClick = useLastCallback(() => {
openGiftPremiumModal({ forUserIds: [chatId] }); openPremiumGiftModal({ forUserIds: [chatId] });
closeMenu(); closeMenu();
}); });

View File

@ -277,6 +277,11 @@
margin-inline: auto; margin-inline: auto;
} }
.action-message-stars-gift {
width: 15rem;
margin-inline: auto;
}
.action-message-subtitle { .action-message-subtitle {
margin-top: 1rem; margin-top: 1rem;
font-weight: normal; font-weight: normal;

View File

@ -82,7 +82,8 @@ import Composer from '../common/Composer';
import PrivacySettingsNoticeModal from '../common/PrivacySettingsNoticeModal.async'; import PrivacySettingsNoticeModal from '../common/PrivacySettingsNoticeModal.async';
import SeenByModal from '../common/SeenByModal.async'; import SeenByModal from '../common/SeenByModal.async';
import UnpinAllMessagesModal from '../common/UnpinAllMessagesModal.async'; import UnpinAllMessagesModal from '../common/UnpinAllMessagesModal.async';
import GiftPremiumModal from '../main/premium/GiftPremiumModal.async'; import PremiumGiftModal from '../main/premium/PremiumGiftModal.async';
import StarsGiftModal from '../main/premium/StarsGiftModal.async';
import Button from '../ui/Button'; import Button from '../ui/Button';
import Transition from '../ui/Transition'; import Transition from '../ui/Transition';
import ChatLanguageModal from './ChatLanguageModal.async'; import ChatLanguageModal from './ChatLanguageModal.async';
@ -133,7 +134,8 @@ type StateProps = {
isSeenByModalOpen: boolean; isSeenByModalOpen: boolean;
isPrivacySettingsNoticeModalOpen: boolean; isPrivacySettingsNoticeModalOpen: boolean;
isReactorListModalOpen: boolean; isReactorListModalOpen: boolean;
isGiftPremiumModalOpen?: boolean; isPremiumGiftModalOpen?: boolean;
isStarsGiftModalOpen?: boolean;
isChatLanguageModalOpen?: boolean; isChatLanguageModalOpen?: boolean;
withInterfaceAnimations?: boolean; withInterfaceAnimations?: boolean;
shouldSkipHistoryAnimations?: boolean; shouldSkipHistoryAnimations?: boolean;
@ -193,7 +195,8 @@ function MiddleColumn({
isSeenByModalOpen, isSeenByModalOpen,
isPrivacySettingsNoticeModalOpen, isPrivacySettingsNoticeModalOpen,
isReactorListModalOpen, isReactorListModalOpen,
isGiftPremiumModalOpen, isPremiumGiftModalOpen,
isStarsGiftModalOpen,
isChatLanguageModalOpen, isChatLanguageModalOpen,
withInterfaceAnimations, withInterfaceAnimations,
shouldSkipHistoryAnimations, shouldSkipHistoryAnimations,
@ -715,7 +718,8 @@ function MiddleColumn({
/> />
))} ))}
</div> </div>
<GiftPremiumModal isOpen={isGiftPremiumModalOpen} /> <PremiumGiftModal isOpen={isPremiumGiftModalOpen} />
<StarsGiftModal isOpen={isStarsGiftModalOpen} />
</div> </div>
); );
} }
@ -729,7 +733,7 @@ export default memo(withGlobal<OwnProps>(
const { const {
messageLists, isLeftColumnShown, activeEmojiInteractions, messageLists, isLeftColumnShown, activeEmojiInteractions,
seenByModal, giftPremiumModal, reactorModal, audioPlayer, shouldSkipHistoryAnimations, seenByModal, giftModal, starsGiftModal, reactorModal, audioPlayer, shouldSkipHistoryAnimations,
chatLanguageModal, privacySettingsNoticeModal, chatLanguageModal, privacySettingsNoticeModal,
} = selectTabState(global); } = selectTabState(global);
const currentMessageList = selectCurrentMessageList(global); const currentMessageList = selectCurrentMessageList(global);
@ -748,7 +752,8 @@ export default memo(withGlobal<OwnProps>(
isSeenByModalOpen: Boolean(seenByModal), isSeenByModalOpen: Boolean(seenByModal),
isPrivacySettingsNoticeModalOpen: Boolean(privacySettingsNoticeModal), isPrivacySettingsNoticeModalOpen: Boolean(privacySettingsNoticeModal),
isReactorListModalOpen: Boolean(reactorModal), isReactorListModalOpen: Boolean(reactorModal),
isGiftPremiumModalOpen: giftPremiumModal?.isOpen, isPremiumGiftModalOpen: giftModal?.isOpen,
isStarsGiftModalOpen: starsGiftModal?.isOpen,
isChatLanguageModalOpen: Boolean(chatLanguageModal), isChatLanguageModalOpen: Boolean(chatLanguageModal),
withInterfaceAnimations: selectCanAnimateInterface(global), withInterfaceAnimations: selectCanAnimateInterface(global),
currentTransitionKey: Math.max(0, messageLists.length - 1), currentTransitionKey: Math.max(0, messageLists.length - 1),

View File

@ -1,3 +1,5 @@
@use '../../../styles/mixins';
.content { .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -19,10 +21,30 @@
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
} }
.section {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.5rem;
position: relative;
@include mixins.adapt-padding-to-scrollbar(0.5rem);
}
.logo { .logo {
margin: 1rem;
width: 6.25rem; width: 6.25rem;
height: 6.25rem; height: 6.25rem;
align-self: center; min-height: 6.25rem;
}
.logoBackground {
position: absolute;
top: 0.75rem;
left: 50%;
transform: translateX(-50%);
height: 8rem;
} }
.avatar { .avatar {

View File

@ -15,6 +15,8 @@ import Modal from '../../ui/Modal';
import styles from './TableInfoModal.module.scss'; import styles from './TableInfoModal.module.scss';
import StarsBackground from '../../../assets/stars-bg.png';
type ChatItem = { chatId: string }; type ChatItem = { chatId: string };
export type TableData = [TeactNode, TeactNode | ChatItem][]; export type TableData = [TeactNode, TeactNode | ChatItem][];
@ -24,6 +26,7 @@ type OwnProps = {
title?: string; title?: string;
tableData?: TableData; tableData?: TableData;
headerImageUrl?: string; headerImageUrl?: string;
logoBackground?: string;
headerAvatarPeer?: ApiPeer | CustomPeer; headerAvatarPeer?: ApiPeer | CustomPeer;
headerAvatarWebPhoto?: ApiWebDocument; headerAvatarWebPhoto?: ApiWebDocument;
noHeaderImage?: boolean; noHeaderImage?: boolean;
@ -40,6 +43,7 @@ const TableInfoModal = ({
title, title,
tableData, tableData,
headerImageUrl, headerImageUrl,
logoBackground,
headerAvatarPeer, headerAvatarPeer,
headerAvatarWebPhoto, headerAvatarWebPhoto,
noHeaderImage, noHeaderImage,
@ -73,7 +77,11 @@ const TableInfoModal = ({
withAvatar ? ( withAvatar ? (
<Avatar peer={headerAvatarPeer} webPhoto={headerAvatarWebPhoto} size="jumbo" className={styles.avatar} /> <Avatar peer={headerAvatarPeer} webPhoto={headerAvatarWebPhoto} size="jumbo" className={styles.avatar} />
) : ( ) : (
<img className={styles.logo} src={headerImageUrl} alt="" draggable={false} /> <div className={styles.section}>
<img className={styles.logo} src={headerImageUrl} alt="" draggable={false} />
{Boolean(logoBackground)
&& <img className={styles.logoBackground} src={StarsBackground} alt="" draggable={false} />}
</div>
) )
)} )}
{header} {header}
@ -98,7 +106,7 @@ const TableInfoModal = ({
</table> </table>
{footer} {footer}
{buttonText && ( {buttonText && (
<Button onClick={onButtonClick || onClose}>{buttonText}</Button> <Button size="smaller" onClick={onButtonClick || onClose}>{buttonText}</Button>
)} )}
</Modal> </Modal>
); );

View File

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

View File

@ -0,0 +1,32 @@
@use '../../../styles/mixins';
.centered {
text-align: center !important;
}
.section {
display: flex;
flex-direction: column;
align-items: center;
padding: 0.5rem;
position: relative;
@include mixins.adapt-padding-to-scrollbar(0.5rem);
}
.info {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 0.5rem;
}
.starTitle {
margin: 0;
}
.footer {
margin: 1rem 0;
color: var(--color-text-secondary);
}

View File

@ -0,0 +1,150 @@
import React, { memo, useMemo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiPeer } from '../../../api/types';
import { getSenderTitle } from '../../../global/helpers';
import { selectTabState, selectUser } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { formatDateTimeToString } from '../../../util/dates/dateFormat';
import renderText from '../../common/helpers/renderText';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import StarIcon from '../../common/icons/StarIcon';
import SafeLink from '../../common/SafeLink';
import TableInfoModal, { type TableData } from '../common/TableInfoModal';
import styles from './StarGiftInfoModal.module.scss';
import StarLogo from '../../../assets/icons/StarLogo.svg';
import StarsBackground from '../../../assets/stars-bg.png';
export type OwnProps = {
isOpen?: boolean;
};
export type StateProps = {
stars?: number;
user?: ApiPeer;
date?: number;
};
const StarGiftInfoModal = ({
isOpen,
stars,
user,
date,
}: OwnProps & StateProps) => {
const {
closeStarGiftInfoModal,
} = getActions();
const oldLang = useOldLang();
const lang = useLang();
const infoText = useMemo(() => {
const linkText = oldLang('GiftStarsSubtitleLinkName');
return lang('CreditsBoxHistoryEntryGiftOutAbout',
{
user: (
<b>
{user && renderText(getSenderTitle(oldLang, user) || '', ['simple_markdown'])}
</b>
),
link: (
<SafeLink
url={oldLang('lng_paid_about_link_url')}
text={linkText}
/>
),
},
{
withNodes: true,
});
}, [lang, oldLang, user]);
const footerText = useMemo(() => {
const linkText = oldLang('lng_payments_terms_link');
return lang('CreditsBoxOutAbout', {
link: (
<SafeLink
url={oldLang('StarsTOSLink')}
text={linkText}
/>
),
}, {
withNodes: true,
});
}, [lang, oldLang]);
const handleButtonClick = useLastCallback(() => {
closeStarGiftInfoModal();
});
const modalData = useMemo(() => {
if (!isOpen) return undefined;
const header = (
<>
<h2 className={buildClassName(styles.starTitle, styles.centered)}>{oldLang('StarsGiftSent')}</h2>
<div className={styles.info}>
<p className={buildClassName(styles.starTitle, styles.centered)}>{stars}</p>
<StarIcon type="gold" size="middle" />
</div>
<p className={styles.centered}>{infoText}</p>
</>
);
const tableData = [
[oldLang('Recipient'), user ? { chatId: user.id } : oldLang('BoostingNoRecipient')],
[oldLang('BoostingDate'), formatDateTimeToString(date! * 1000, lang.code, true)],
] satisfies TableData;
const footer = (
<span className={buildClassName(styles.footer, styles.centered)}>
{footerText}
</span>
);
return {
header,
tableData,
footer,
};
}, [isOpen, oldLang, stars, infoText, user, date, lang.code, footerText]);
if (!modalData) return undefined;
return (
<TableInfoModal
isOpen={isOpen}
headerImageUrl={StarLogo}
logoBackground={StarsBackground}
tableData={modalData.tableData}
header={modalData.header}
footer={modalData.footer}
buttonText={lang('Close')}
onButtonClick={handleButtonClick}
onClose={closeStarGiftInfoModal}
/>
);
};
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const {
starGiftInfoModal,
} = selectTabState(global);
const toUserId = starGiftInfoModal?.toUserId;
const user = toUserId ? selectUser(global, toUserId) : undefined;
return {
stars: starGiftInfoModal?.stars,
user,
date: starGiftInfoModal?.date,
};
},
)(StarGiftInfoModal));

View File

@ -0,0 +1,62 @@
@use '../../../styles/mixins';
.option {
--_background-color: var(--color-background-secondary);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.125rem;
padding: 1rem;
border-radius: 0.625rem;
background-color: var(--_background-color);
transition: background-color 0.25s ease-out;
cursor: var(--custom-cursor, pointer);
&:hover {
--_background-color: var(--color-background-secondary-accent);
}
}
.wideOption {
grid-column: 1 / -1;
}
.optionTop {
display: flex;
align-items: center;
gap: 0.25rem;
font-weight: 500;
font-size: 1.5rem;
line-height: 1;
}
.stackedStars {
display: grid;
grid-auto-columns: 0.4375rem;
grid-auto-flow: column;
justify-items: end;
}
.stackedStar {
@include mixins.filter-outline(0.0625rem, var(--_background-color));
transition: filter 0.25s ease-out;
}
.optionBottom {
font-size: 0.875rem;
color: var(--color-text-secondary);
}
.moreOptions {
grid-column: 1/-1;
}
.iconDown {
margin-inline-start: 0.25rem;
font-size: 1.5rem;
}

View File

@ -0,0 +1,110 @@
import type { FC } from '../../../lib/teact/teact';
import React, { memo, useEffect, useMemo } from '../../../lib/teact/teact';
import type { ApiStarTopupOption } from '../../../api/types';
import buildClassName from '../../../util/buildClassName';
import { formatCurrency } from '../../../util/formatCurrency';
import { formatInteger } from '../../../util/textFormat';
import useFlag from '../../../hooks/useFlag';
import useOldLang from '../../../hooks/useOldLang';
import Icon from '../../common/icons/Icon';
import StarIcon from '../../common/icons/StarIcon';
import Button from '../../ui/Button';
import styles from './StarTopupOptionList.module.scss';
const MAX_STARS_COUNT = 6;
type OwnProps = {
isActive?: boolean;
options?: ApiStarTopupOption[];
starsNeeded?: number;
onClick: (option: ApiStarTopupOption) => void;
};
const StarTopupOptionList: FC<OwnProps> = ({
isActive,
options,
starsNeeded,
onClick,
}) => {
const lang = useOldLang();
const [areOptionsExtended, markOptionsExtended, unmarkOptionsExtended] = useFlag();
useEffect(() => {
if (!isActive) {
unmarkOptionsExtended();
}
}, [isActive]);
const [renderingOptions, canExtend] = useMemo(() => {
if (!options) {
return [undefined, false];
}
const maxOption = options.reduce((max, option) => (
max.stars > option.stars ? max : option
));
const forceShowAll = starsNeeded && maxOption.stars < starsNeeded;
const result: { option: ApiStarTopupOption; starsCount: number; isWide: boolean }[] = [];
let currentStackedStarsCount = 0;
let canExtendOptions = false;
options.forEach((option, index) => {
if (!option.isExtended) currentStackedStarsCount++;
if (starsNeeded && !forceShowAll && option.stars < starsNeeded) return;
if (!areOptionsExtended && option.isExtended) {
canExtendOptions = true;
return;
}
result.push({
option,
starsCount: Math.min(currentStackedStarsCount, MAX_STARS_COUNT),
isWide: index === options.length - 1,
});
});
return [result, canExtendOptions];
}, [areOptionsExtended, options, starsNeeded]);
return (
<>
{renderingOptions?.map(({ option, starsCount, isWide }) => {
const length = renderingOptions?.length;
const isOdd = length % 2 === 0;
return (
<div
className={buildClassName(styles.option, (!isOdd && isWide) && styles.wideOption)}
key={option.stars}
onClick={() => onClick?.(option)}
>
<div className={styles.optionTop}>
+{formatInteger(option.stars)}
<div className={styles.stackedStars} dir={lang.isRtl ? 'ltr' : 'rtl'}>
{Array.from({ length: starsCount }).map(() => (
<StarIcon className={styles.stackedStar} type="gold" size="big" />
))}
</div>
</div>
<div className={styles.optionBottom}>
{formatCurrency(option.amount, option.currency, lang.code)}
</div>
</div>
);
})}
{!areOptionsExtended && canExtend && (
<Button className={styles.moreOptions} isText noForcedUpperCase onClick={markOptionsExtended}>
{lang('Stars.Purchase.ShowMore')}
<Icon className={styles.iconDown} name="down" />
</Button>
)}
</>
);
};
export default memo(StarTopupOptionList);

View File

@ -24,6 +24,11 @@
overflow-y: scroll; overflow-y: scroll;
} }
.container {
display: flex;
flex-direction: column-reverse;
}
.section { .section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -138,63 +143,16 @@
width: 100%; width: 100%;
} }
.option { .optionFullWidth {
--_background-color: var(--color-background-secondary); grid-column: 1 / -1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.125rem;
padding: 1rem;
border-radius: 0.625rem;
background-color: var(--_background-color);
transition: background-color 0.25s ease-out;
cursor: var(--custom-cursor, pointer);
&:hover {
--_background-color: var(--color-background-secondary-accent);
}
} }
.optionTop { .starButton {
display: flex;
align-items: center;
gap: 0.25rem;
font-weight: 500;
font-size: 1.5rem;
line-height: 1;
white-space: nowrap;
}
.stackedStars {
display: grid;
grid-auto-columns: 0.4375rem;
grid-auto-flow: column;
justify-items: center;
margin-left: 0.375rem;
}
.stackedStar {
@include mixins.filter-outline(0.0625rem, var(--_background-color));
transition: filter 0.25s ease-out;
}
.optionBottom {
font-size: 0.875rem;
color: var(--color-text-secondary);
}
.moreOptions {
grid-column: 1/-1; grid-column: 1/-1;
} display: flex;
justify-content: center;
.iconDown { align-items: center;
margin-inline-start: 0.25rem; gap: 1rem;
font-size: 1.5rem;
} }
.paymentContent { .paymentContent {

View File

@ -7,13 +7,11 @@ import type { ApiStarTopupOption, ApiUser } from '../../../api/types';
import type { GlobalState, TabState } from '../../../global/types'; import type { GlobalState, TabState } from '../../../global/types';
import { getUserFullName } from '../../../global/helpers'; import { getUserFullName } from '../../../global/helpers';
import { selectUser } from '../../../global/selectors'; import { selectIsPremiumPurchaseBlocked, selectUser } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { formatCurrency } from '../../../util/formatCurrency';
import { formatInteger } from '../../../util/textFormat';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
import useFlag from '../../../hooks/useFlag'; import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
@ -27,6 +25,7 @@ import TabList, { type TabWithProperties } from '../../ui/TabList';
import Transition from '../../ui/Transition'; import Transition from '../../ui/Transition';
import BalanceBlock from './BalanceBlock'; import BalanceBlock from './BalanceBlock';
import TransactionItem from './StarsTransactionItem'; import TransactionItem from './StarsTransactionItem';
import StarTopupOptionList from './StarTopupOptionList';
import styles from './StarsBalanceModal.module.scss'; import styles from './StarsBalanceModal.module.scss';
@ -39,7 +38,6 @@ const TRANSACTION_TABS: TabWithProperties[] = [
{ title: 'StarsTransactionsIncoming' }, { title: 'StarsTransactionsIncoming' },
{ title: 'StarsTransactionsOutgoing' }, { title: 'StarsTransactionsOutgoing' },
]; ];
const MAX_STARS_COUNT = 6;
export type OwnProps = { export type OwnProps = {
modal: TabState['starsBalanceModal']; modal: TabState['starsBalanceModal'];
@ -48,19 +46,22 @@ export type OwnProps = {
type StateProps = { type StateProps = {
starsBalanceState?: GlobalState['stars']; starsBalanceState?: GlobalState['stars'];
originPaymentBot?: ApiUser; originPaymentBot?: ApiUser;
canBuyPremium?: boolean;
}; };
const StarsBalanceModal = ({ const StarsBalanceModal = ({
modal, starsBalanceState, originPaymentBot, modal, starsBalanceState, originPaymentBot, canBuyPremium,
}: OwnProps & StateProps) => { }: OwnProps & StateProps) => {
const { closeStarsBalanceModal, loadStarsTransactions, openInvoice } = getActions(); const {
closeStarsBalanceModal, loadStarsTransactions, openInvoice, openStarsGiftingModal,
} = getActions();
const { balance, history, topupOptions } = starsBalanceState || {}; const { balance, history, topupOptions } = starsBalanceState || {};
const lang = useOldLang(); const oldLang = useOldLang();
const lang = useLang();
const [isHeaderHidden, setHeaderHidden] = useState(true); const [isHeaderHidden, setHeaderHidden] = useState(true);
const [areOptionsExtended, markOptionsExtended, unmarkOptionsExtended] = useFlag();
const [selectedTabIndex, setSelectedTabIndex] = useState(0); const [selectedTabIndex, setSelectedTabIndex] = useState(0);
const isOpen = Boolean(modal && starsBalanceState); const isOpen = Boolean(modal && starsBalanceState);
@ -73,52 +74,21 @@ const StarsBalanceModal = ({
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
setHeaderHidden(true); setHeaderHidden(true);
unmarkOptionsExtended();
setSelectedTabIndex(0); setSelectedTabIndex(0);
} }
}, [isOpen]); }, [isOpen]);
const [renderingOptions, canExtend] = useMemo(() => {
if (!topupOptions) {
return [undefined, false];
}
const maxOption = topupOptions.reduce((max, option) => (
max.stars > option.stars ? max : option
));
const forceShowAll = starsNeeded && maxOption.stars < starsNeeded;
const result: { option: ApiStarTopupOption; starsCount: number }[] = [];
let currentStackedStarsCount = 0;
let canExtendOptions = false;
topupOptions.forEach((option) => {
if (!option.isExtended) currentStackedStarsCount++;
if (starsNeeded && !forceShowAll && option.stars < starsNeeded) return;
if (!areOptionsExtended && option.isExtended) {
canExtendOptions = true;
return;
}
result.push({
option,
starsCount: Math.min(currentStackedStarsCount, MAX_STARS_COUNT),
});
});
return [result, canExtendOptions];
}, [areOptionsExtended, topupOptions, starsNeeded]);
const tosText = useMemo(() => { const tosText = useMemo(() => {
if (!isOpen) return undefined; if (!isOpen) return undefined;
const text = lang('lng_credits_summary_options_about'); const text = oldLang('lng_credits_summary_options_about');
const parts = text.split('{link}'); const parts = text.split('{link}');
return [ return [
parts[0], parts[0],
<SafeLink url={lang('StarsTOSLink')} text={lang('lng_credits_summary_options_about_link')} />, <SafeLink url={oldLang('StarsTOSLink')} text={oldLang('lng_credits_summary_options_about_link')} />,
parts[1], parts[1],
]; ];
}, [isOpen, lang]); }, [isOpen, oldLang]);
function handleScroll(e: React.UIEvent<HTMLDivElement>) { function handleScroll(e: React.UIEvent<HTMLDivElement>) {
const { scrollTop } = e.currentTarget; const { scrollTop } = e.currentTarget;
@ -135,12 +105,27 @@ const StarsBalanceModal = ({
}); });
}); });
function renderStarOptionList() {
return (
<StarTopupOptionList
isActive={isOpen}
options={topupOptions}
starsNeeded={starsNeeded}
onClick={handleClick}
/>
);
}
const handleLoadMore = useLastCallback(() => { const handleLoadMore = useLastCallback(() => {
loadStarsTransactions({ loadStarsTransactions({
type: TRANSACTION_TYPES[selectedTabIndex], type: TRANSACTION_TYPES[selectedTabIndex],
}); });
}); });
const openPremiumGiftingModalHandler = useLastCallback(() => {
openStarsGiftingModal({});
});
return ( return (
<Modal className={styles.root} isOpen={isOpen} onClose={closeStarsBalanceModal}> <Modal className={styles.root} isOpen={isOpen} onClose={closeStarsBalanceModal}>
<div className={buildClassName(styles.main, 'custom-scroll')} onScroll={handleScroll}> <div className={buildClassName(styles.main, 'custom-scroll')} onScroll={handleScroll}>
@ -158,29 +143,31 @@ const StarsBalanceModal = ({
<BalanceBlock balance={balance || 0} className={styles.modalBalance} /> <BalanceBlock balance={balance || 0} className={styles.modalBalance} />
<div className={buildClassName(styles.header, isHeaderHidden && styles.hiddenHeader)}> <div className={buildClassName(styles.header, isHeaderHidden && styles.hiddenHeader)}>
<h2 className={styles.starHeaderText}> <h2 className={styles.starHeaderText}>
{lang('TelegramStars')} {oldLang('TelegramStars')}
</h2> </h2>
</div> </div>
<div className={styles.section}> <div className={styles.section}>
<img className={styles.logo} src={StarLogo} alt="" draggable={false} /> <img className={styles.logo} src={StarLogo} alt="" draggable={false} />
<img className={styles.logoBackground} src={StarsBackground} alt="" draggable={false} /> <img className={styles.logoBackground} src={StarsBackground} alt="" draggable={false} />
<h2 className={styles.headerText}> <h2 className={styles.headerText}>
{starsNeeded ? lang('StarsNeededTitle', starsNeeded) : lang('TelegramStars')} {starsNeeded ? oldLang('StarsNeededTitle', starsNeeded) : oldLang('TelegramStars')}
</h2> </h2>
<div className={styles.description}> <div className={styles.description}>
{renderText( {renderText(
starsNeeded ? lang('StarsNeededText', originBotName) : lang('TelegramStarsInfo'), starsNeeded ? oldLang('StarsNeededText', originBotName) : oldLang('TelegramStarsInfo'),
['simple_markdown', 'emoji'], ['simple_markdown', 'emoji'],
)} )}
</div> </div>
<div className={styles.options}> <div className={styles.options}>
{renderingOptions?.map(({ option, starsCount }) => ( {renderStarOptionList()}
<StarTopupOption option={option} starsCount={starsCount} onClick={handleClick} /> {canBuyPremium && (
))} <Button
{!areOptionsExtended && canExtend && ( className={buildClassName(styles.starButton, 'settings-main-menu-star')}
<Button className={styles.moreOptions} isText noForcedUpperCase onClick={markOptionsExtended}> // eslint-disable-next-line react/jsx-no-bind
{lang('Stars.Purchase.ShowMore')} onClick={openPremiumGiftingModalHandler}
<Icon className={styles.iconDown} name="down" /> >
<StarIcon className="icon" type="gold" size="big" />
{oldLang('TelegramStarsGift')}
</Button> </Button>
)} )}
</div> </div>
@ -189,13 +176,7 @@ const StarsBalanceModal = ({
{tosText} {tosText}
</div> </div>
{shouldShowTransactions && ( {shouldShowTransactions && (
<> <div className={styles.container}>
<TabList
big
activeTab={selectedTabIndex}
tabs={TRANSACTION_TABS}
onSwitchTab={setSelectedTabIndex}
/>
<div className={styles.section}> <div className={styles.section}>
<Transition <Transition
name={lang.isRtl ? 'slideOptimizedRtl' : 'slideOptimized'} name={lang.isRtl ? 'slideOptimizedRtl' : 'slideOptimized'}
@ -219,38 +200,19 @@ const StarsBalanceModal = ({
</InfiniteScroll> </InfiniteScroll>
</Transition> </Transition>
</div> </div>
</> <TabList
big
activeTab={selectedTabIndex}
tabs={TRANSACTION_TABS}
onSwitchTab={setSelectedTabIndex}
/>
</div>
)} )}
</div> </div>
</Modal> </Modal>
); );
}; };
function StarTopupOption({
option, starsCount, onClick,
}: {
option: ApiStarTopupOption; starsCount: number; onClick?: (option: ApiStarTopupOption) => void;
}) {
const lang = useOldLang();
return (
<div className={styles.option} key={option.stars} onClick={() => onClick?.(option)}>
<div className={styles.optionTop}>
+{formatInteger(option.stars)}
{/* Switch directionality for correct order. Can't use flex because https://issues.chromium.org/issues/40249030 */}
<div className={styles.stackedStars} dir={lang.isRtl ? 'ltr' : 'rtl'}>
{Array.from({ length: starsCount }).map(() => (
<StarIcon className={styles.stackedStar} type="gold" size="big" />
))}
</div>
</div>
<div className={styles.optionBottom}>
{formatCurrency(option.amount, option.currency, lang.code)}
</div>
</div>
);
}
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { modal }): StateProps => { (global, { modal }): StateProps => {
const botId = modal?.originPayment?.botId; const botId = modal?.originPayment?.botId;
@ -259,6 +221,7 @@ export default memo(withGlobal<OwnProps>(
return { return {
starsBalanceState: global.stars, starsBalanceState: global.stars,
originPaymentBot: bot, originPaymentBot: bot,
canBuyPremium: !selectIsPremiumPurchaseBlocked(global),
}; };
}, },
)(StarsBalanceModal)); )(StarsBalanceModal));

View File

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

View File

@ -511,7 +511,63 @@ addActionHandler('closePremiumGiftingModal', (global, actions, payload): ActionR
}, tabId); }, tabId);
}); });
addActionHandler('openGiftPremiumModal', async (global, actions, payload): Promise<void> => { addActionHandler('openStarsGiftingModal', (global, actions, payload): ActionReturnType => {
const {
tabId = getCurrentTabId(),
} = payload || {};
global = getGlobal();
global = updateTabState(global, {
starsGiftingModal: {
isOpen: true,
},
}, tabId);
setGlobal(global);
});
addActionHandler('closeStarsGiftingModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
starsGiftingModal: undefined,
}, tabId);
});
addActionHandler('openStarGiftInfoModal', (global, actions, payload): ActionReturnType => {
const {
toUserId,
stars,
date,
tabId = getCurrentTabId(),
} = payload || {};
if (!stars || !toUserId || !date) {
return;
}
global = getGlobal();
global = updateTabState(global, {
starGiftInfoModal: {
toUserId,
stars,
date,
isOpen: true,
},
}, tabId);
setGlobal(global);
});
addActionHandler('closeStarGiftInfoModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
return updateTabState(global, {
starGiftInfoModal: undefined,
}, tabId);
});
addActionHandler('openPremiumGiftModal', async (global, actions, payload): Promise<void> => {
const { const {
forUserIds, tabId = getCurrentTabId(), forUserIds, tabId = getCurrentTabId(),
} = payload || {}; } = payload || {};
@ -524,7 +580,7 @@ addActionHandler('openGiftPremiumModal', async (global, actions, payload): Promi
const gifts = await callApi('getPremiumGiftCodeOptions', {}); const gifts = await callApi('getPremiumGiftCodeOptions', {});
global = updateTabState(global, { global = updateTabState(global, {
giftPremiumModal: { giftModal: {
isOpen: true, isOpen: true,
forUserIds, forUserIds,
gifts, gifts,
@ -533,10 +589,35 @@ addActionHandler('openGiftPremiumModal', async (global, actions, payload): Promi
setGlobal(global); setGlobal(global);
}); });
addActionHandler('closeGiftPremiumModal', (global, actions, payload): ActionReturnType => { addActionHandler('closePremiumGiftModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {}; const { tabId = getCurrentTabId() } = payload || {};
global = updateTabState(global, { global = updateTabState(global, {
giftPremiumModal: { isOpen: false }, giftModal: { isOpen: false },
}, tabId);
setGlobal(global);
});
addActionHandler('openStarsGiftModal', async (global, actions, payload): Promise<void> => {
const {
forUserId, tabId = getCurrentTabId(),
} = payload || {};
const starsGiftOptions = await callApi('getStarsGiftOptions', {});
global = updateTabState(global, {
starsGiftModal: {
isOpen: true,
forUserId,
starsGiftOptions,
},
}, tabId);
setGlobal(global);
});
addActionHandler('closeStarsGiftModal', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
global = updateTabState(global, {
starsGiftModal: { isOpen: false },
}, tabId); }, tabId);
setGlobal(global); setGlobal(global);
}); });

View File

@ -32,12 +32,12 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
if (!inputInvoice.userIds) { if (!inputInvoice.userIds) {
return; return;
} }
const giftModalState = selectTabState(global, tabId).giftPremiumModal; const giftModalState = selectTabState(global, tabId).giftModal;
if (giftModalState && giftModalState.isOpen if (giftModalState && giftModalState.isOpen
&& areDeepEqual(inputInvoice.userIds, giftModalState.forUserIds)) { && areDeepEqual(inputInvoice.userIds, giftModalState.forUserIds)) {
global = updateTabState(global, { global = updateTabState(global, {
giftPremiumModal: { giftModal: {
...giftModalState, ...giftModalState,
isCompleted: true, isCompleted: true,
}, },
@ -46,6 +46,24 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
} }
} }
if (inputInvoice?.type === 'starsgift') {
if (!inputInvoice.userId) {
return;
}
const starsModalState = selectTabState(global, tabId).starsGiftModal;
if (starsModalState && starsModalState.isOpen
&& areDeepEqual(inputInvoice.userId, starsModalState.forUserId)) {
global = updateTabState(global, {
starsGiftModal: {
...starsModalState,
isCompleted: true,
},
}, tabId);
global = closeInvoice(global, tabId);
}
}
setGlobal(global); setGlobal(global);
}); });

View File

@ -12,6 +12,26 @@ export function getRequestInputInvoice<T extends GlobalState>(
): ApiRequestInputInvoice | undefined { ): ApiRequestInputInvoice | undefined {
if (inputInvoice.type === 'slug') return inputInvoice; if (inputInvoice.type === 'slug') return inputInvoice;
if (inputInvoice.type === 'starsgift') {
const {
userId, stars, amount, currency,
} = inputInvoice;
const user = selectUser(global, userId!);
if (!user) return undefined;
return {
type: 'stars',
purpose: {
type: 'starsgift',
user,
stars,
amount,
currency,
},
};
}
if (inputInvoice.type === 'stars') { if (inputInvoice.type === 'stars') {
const { const {
stars, amount, currency, stars, amount, currency,

View File

@ -61,7 +61,7 @@ import type {
ApiSendMessageAction, ApiSendMessageAction,
ApiSession, ApiSession,
ApiSessionData, ApiSessionData,
ApiSponsoredMessage, ApiSponsoredMessage, ApiStarsGiftOption,
ApiStarsTransaction, ApiStarsTransaction,
ApiStarTopupOption, ApiStarTopupOption,
ApiStealthMode, ApiStealthMode,
@ -709,11 +709,30 @@ export type TabState = {
isOpen?: boolean; isOpen?: boolean;
}; };
giftPremiumModal?: { starsGiftingModal?: {
isOpen?: boolean;
};
starGiftInfoModal?: {
isOpen?: boolean;
toUserId: string;
date: number;
stars: number;
};
starsGiftModal?: {
isCompleted?: boolean;
isOpen?: boolean;
forUserId?: string;
starsGiftOptions?: ApiStarsGiftOption[];
};
giftModal?: {
isCompleted?: boolean; isCompleted?: boolean;
isOpen?: boolean; isOpen?: boolean;
forUserIds?: string[]; forUserIds?: string[];
gifts?: ApiPremiumGiftCodeOption[]; gifts?: ApiPremiumGiftCodeOption[];
starsGiftOptions?: ApiStarsGiftOption[];
}; };
limitReachedModal?: { limitReachedModal?: {
@ -3162,6 +3181,9 @@ export interface ActionPayloads {
openPremiumGiftingModal: WithTabId | undefined; openPremiumGiftingModal: WithTabId | undefined;
closePremiumGiftingModal: WithTabId | undefined; closePremiumGiftingModal: WithTabId | undefined;
openStarsGiftingModal: WithTabId | undefined;
closeStarsGiftingModal: WithTabId | undefined;
openDeleteMessageModal: ({ openDeleteMessageModal: ({
message?: ApiMessage; message?: ApiMessage;
isSchedule?: boolean; isSchedule?: boolean;
@ -3179,12 +3201,26 @@ export interface ActionPayloads {
loadDefaultTopicIcons: undefined; loadDefaultTopicIcons: undefined;
loadPremiumStickers: undefined; loadPremiumStickers: undefined;
openGiftPremiumModal: ({ openPremiumGiftModal: ({
chatId?: string; chatId?: string;
forMultipleUsers?: boolean; forMultipleUsers?: boolean;
forUserIds?: string[]; forUserIds?: string[];
isStarsGifting?: boolean;
} & WithTabId) | undefined; } & WithTabId) | undefined;
closeGiftPremiumModal: WithTabId | undefined; closePremiumGiftModal: WithTabId | undefined;
openStarsGiftModal: ({
chatId?: string;
forUserId?: string;
} & WithTabId) | undefined;
closeStarsGiftModal: WithTabId | undefined;
openStarGiftInfoModal: ({
toUserId?: string;
stars?: number;
date?: number;
} & WithTabId) | undefined;
closeStarGiftInfoModal: WithTabId | undefined;
setEmojiStatus: { setEmojiStatus: {
emojiStatus: ApiSticker; emojiStatus: ApiSticker;

View File

@ -1514,7 +1514,16 @@ export interface LangPair {
'ReplyInPrivateMessage': undefined; 'ReplyInPrivateMessage': undefined;
'AriaSearchOlderResult': undefined; 'AriaSearchOlderResult': undefined;
'AriaSearchNewerResult': undefined; 'AriaSearchNewerResult': undefined;
'CreditsBoxHistoryEntryGiftOutAbout': {
'user': string | number;
'link': string | number;
};
'CreditsBoxOutAbout': {
'link': string | number;
};
'GiftStarsOutgoing': {
'user': string | number;
};
} }
export type LangKey = keyof LangPair; export type LangKey = keyof LangPair;

View File

@ -91,6 +91,7 @@ async function loadFallbackPack() {
if (!language) { if (!language) {
updateLanguage(fallbackData.language); updateLanguage(fallbackData.language);
} else { } else {
translationFn = createTranslationFn();
scheduleCallbacks(); scheduleCallbacks();
} }
} }