Support paid messages (#5712)
This commit is contained in:
parent
dc61b12f9b
commit
5bb202857d
@ -87,6 +87,10 @@ export interface GramJsAppConfig extends LimitsConfig {
|
||||
stargifts_convert_period_max?: number;
|
||||
starref_start_param_prefixes?: string[];
|
||||
ton_blockchain_explorer_url?: string;
|
||||
stars_paid_messages_available?: boolean;
|
||||
stars_usd_withdraw_rate_x1000?: number;
|
||||
stars_paid_message_commission_permille?: number;
|
||||
stars_paid_message_amount_max?: number;
|
||||
stargifts_pinned_to_top_limit?: number;
|
||||
}
|
||||
|
||||
@ -164,6 +168,10 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
|
||||
maxPinnedStoriesCount: appConfig.stories_pinned_to_top_count_max,
|
||||
groupTranscribeLevelMin: appConfig.group_transcribe_level_min,
|
||||
canLimitNewMessagesWithoutPremium: appConfig.new_noncontact_peers_require_premium_without_ownpremium,
|
||||
starsPaidMessagesAvailable: appConfig.stars_paid_messages_available,
|
||||
starsPaidMessageCommissionPermille: appConfig.stars_paid_message_commission_permille,
|
||||
starsPaidMessageAmountMax: appConfig.stars_paid_message_amount_max,
|
||||
starsUsdWithdrawRateX1000: appConfig.stars_usd_withdraw_rate_x1000,
|
||||
bandwidthPremiumNotifyPeriod: appConfig.upload_premium_speedup_notify_period,
|
||||
bandwidthPremiumUploadSpeedup: appConfig.upload_premium_speedup_upload,
|
||||
bandwidthPremiumDownloadSpeedup: appConfig.upload_premium_speedup_download,
|
||||
|
||||
@ -75,6 +75,7 @@ function buildApiChatFieldsFromPeerEntity(
|
||||
const boostLevel = ('level' in peerEntity) ? peerEntity.level : undefined;
|
||||
const areProfilesShown = Boolean('signatureProfiles' in peerEntity && peerEntity.signatureProfiles);
|
||||
const subscriptionUntil = 'subscriptionUntilDate' in peerEntity ? peerEntity.subscriptionUntilDate : undefined;
|
||||
const paidMessagesStars = 'sendPaidMessagesStars' in peerEntity ? peerEntity.sendPaidMessagesStars : undefined;
|
||||
|
||||
return {
|
||||
isMin,
|
||||
@ -110,6 +111,7 @@ function buildApiChatFieldsFromPeerEntity(
|
||||
boostLevel,
|
||||
botVerificationIconId,
|
||||
subscriptionUntil,
|
||||
paidMessagesStars: paidMessagesStars?.toJSNumber(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -267,6 +267,7 @@ export function buildApiMessageWithChatId(
|
||||
isInvertedMedia,
|
||||
isVideoProcessingPending,
|
||||
reportDeliveryUntilDate: mtpMessage.reportDeliveryUntilDate,
|
||||
paidMessageStars: mtpMessage.paidMessageStars?.toJSNumber(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -392,6 +393,8 @@ export function buildLocalMessage(
|
||||
story?: ApiStory | ApiStorySkipped,
|
||||
isInvertedMedia?: true,
|
||||
effectId?: string,
|
||||
isPending?: true,
|
||||
messagePriceInStars?: number,
|
||||
) {
|
||||
const localId = getNextLocalMessageId(lastMessageId);
|
||||
const media = attachment && buildUploadingMedia(attachment);
|
||||
@ -427,11 +430,13 @@ export function buildLocalMessage(
|
||||
isForwardingAllowed: true,
|
||||
isInvertedMedia,
|
||||
effectId,
|
||||
...(isPending && { sendingState: 'messageSendingStatePending' }),
|
||||
...(messagePriceInStars && { paidMessageStars: messagePriceInStars }),
|
||||
} satisfies ApiMessage;
|
||||
|
||||
const emojiOnlyCount = getEmojiOnlyCountForMessage(message.content, message.groupedId);
|
||||
|
||||
const finalMessage = {
|
||||
const finalMessage : ApiMessage = {
|
||||
...message,
|
||||
...(emojiOnlyCount && { emojiOnlyCount }),
|
||||
};
|
||||
|
||||
@ -101,6 +101,8 @@ export function buildPrivacyKey(key: GramJs.TypePrivacyKey): ApiPrivacyKey | und
|
||||
return 'birthday';
|
||||
case 'PrivacyKeyStarGiftsAutoSave':
|
||||
return 'gifts';
|
||||
case 'PrivacyKeyNoPaidMessages':
|
||||
return 'noPaidMessages';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@ -535,7 +535,7 @@ export function buildApiStarsTransactionPeer(peer: GramJs.TypeStarsTransactionPe
|
||||
export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): ApiStarsTransaction {
|
||||
const {
|
||||
date, id, peer, stars, description, photo, title, refund, extendedMedia, failed, msgId, pending, gift, reaction,
|
||||
subscriptionPeriod, stargift, giveawayPostId, starrefCommissionPermille, stargiftUpgrade,
|
||||
subscriptionPeriod, stargift, giveawayPostId, starrefCommissionPermille, stargiftUpgrade, paidMessages,
|
||||
} = transaction;
|
||||
|
||||
if (photo) {
|
||||
@ -567,6 +567,7 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction):
|
||||
giveawayPostId,
|
||||
starRefCommision,
|
||||
isGiftUpgrade: stargiftUpgrade,
|
||||
paidMessages,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
|
||||
fallbackPhoto, personalPhoto, translationsDisabled, storiesPinnedAvailable,
|
||||
contactRequirePremium, businessWorkHours, businessLocation, businessIntro,
|
||||
birthday, personalChannelId, personalChannelMessage, sponsoredEnabled, stargiftsCount, botVerification,
|
||||
botCanManageEmojiStatus, settings,
|
||||
botCanManageEmojiStatus, settings, sendPaidMessagesStars,
|
||||
},
|
||||
users,
|
||||
} = mtpUserFull;
|
||||
@ -56,6 +56,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse
|
||||
starGiftCount: stargiftsCount,
|
||||
isBotCanManageEmojiStatus: botCanManageEmojiStatus,
|
||||
hasScheduledMessages: hasScheduled,
|
||||
paidMessagesStars: sendPaidMessagesStars?.toJSNumber(),
|
||||
settings: buildApiPeerSettings(settings),
|
||||
};
|
||||
}
|
||||
@ -69,6 +70,7 @@ export function buildApiPeerSettings({
|
||||
phoneCountry,
|
||||
nameChangeDate,
|
||||
photoChangeDate,
|
||||
chargePaidMessageStars,
|
||||
}: GramJs.PeerSettings): ApiPeerSettings {
|
||||
return {
|
||||
isAutoArchived: Boolean(autoarchived),
|
||||
@ -79,6 +81,7 @@ export function buildApiPeerSettings({
|
||||
phoneCountry,
|
||||
nameChangeDate,
|
||||
photoChangeDate,
|
||||
chargedPaidMessageStars: chargePaidMessageStars?.toJSNumber(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -90,6 +93,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
|
||||
const {
|
||||
id, firstName, lastName, fake, scam, support, closeFriend, storiesUnavailable, storiesMaxId,
|
||||
bot, botActiveUsers, botVerificationIcon, botInlinePlaceholder, botAttachMenu, botCanEdit,
|
||||
sendPaidMessagesStars,
|
||||
} = mtpUser;
|
||||
const hasVideoAvatar = mtpUser.photo instanceof GramJs.UserProfilePhoto ? Boolean(mtpUser.photo.hasVideo) : undefined;
|
||||
const avatarPhotoId = mtpUser.photo && buildAvatarPhotoId(mtpUser.photo);
|
||||
@ -128,6 +132,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
|
||||
botActiveUsers,
|
||||
botVerificationIconId: botVerificationIcon?.toString(),
|
||||
color: mtpUser.color && buildApiPeerColor(mtpUser.color),
|
||||
paidMessagesStars: sendPaidMessagesStars?.toJSNumber(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -488,6 +488,9 @@ export function buildInputPrivacyKey(privacyKey: ApiPrivacyKey) {
|
||||
|
||||
case 'gifts':
|
||||
return new GramJs.InputPrivacyKeyStarGiftsAutoSave();
|
||||
|
||||
case 'noPaidMessages':
|
||||
return new GramJs.InputPrivacyKeyNoPaidMessages();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@ -143,7 +143,7 @@ export async function fetchInlineBotResults({
|
||||
}
|
||||
|
||||
export async function sendInlineBotResult({
|
||||
chat, replyInfo, resultId, queryId, sendAs, isSilent, scheduleDate,
|
||||
chat, replyInfo, resultId, queryId, sendAs, isSilent, scheduleDate, allowPaidStars,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
replyInfo?: ApiInputMessageReplyInfo;
|
||||
@ -152,6 +152,7 @@ export async function sendInlineBotResult({
|
||||
sendAs?: ApiPeer;
|
||||
isSilent?: boolean;
|
||||
scheduleDate?: number;
|
||||
allowPaidStars?: number;
|
||||
}) {
|
||||
const randomId = generateRandomBigInt();
|
||||
|
||||
@ -165,6 +166,7 @@ export async function sendInlineBotResult({
|
||||
replyTo: replyInfo && buildInputReplyTo(replyInfo),
|
||||
...(isSilent && { silent: true }),
|
||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||
...(allowPaidStars && { allowPaidStars: BigInt(allowPaidStars) }),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@ -2,11 +2,14 @@ import BigInt from 'big-integer';
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
import { RPCError } from '../../../lib/gramjs/errors';
|
||||
|
||||
import type { ThreadId, WebPageMediaSize } from '../../../types';
|
||||
import type {
|
||||
ForwardMessagesParams,
|
||||
SendMessageParams,
|
||||
ThreadId,
|
||||
} from '../../../types';
|
||||
import type {
|
||||
ApiAttachment,
|
||||
ApiChat,
|
||||
ApiContact,
|
||||
ApiError,
|
||||
ApiFormattedText,
|
||||
ApiGlobalMessageSearchType,
|
||||
@ -15,18 +18,13 @@ import type {
|
||||
ApiMessageEntity,
|
||||
ApiMessageSearchContext,
|
||||
ApiMessageSearchType,
|
||||
ApiNewPoll,
|
||||
ApiOnProgress,
|
||||
ApiPeer,
|
||||
ApiPoll,
|
||||
ApiReaction,
|
||||
ApiSendMessageAction,
|
||||
ApiSticker,
|
||||
ApiStory,
|
||||
ApiStorySkipped,
|
||||
ApiUser,
|
||||
ApiUserStatus,
|
||||
ApiVideo,
|
||||
MediaContent,
|
||||
} from '../../types';
|
||||
import {
|
||||
@ -255,56 +253,16 @@ export async function fetchMessage({ chat, messageId }: { chat: ApiChat; message
|
||||
|
||||
let mediaQueue = Promise.resolve();
|
||||
|
||||
export function sendMessage(
|
||||
{
|
||||
chat,
|
||||
lastMessageId,
|
||||
text,
|
||||
entities,
|
||||
replyInfo,
|
||||
attachment,
|
||||
sticker,
|
||||
story,
|
||||
gif,
|
||||
poll,
|
||||
contact,
|
||||
isSilent,
|
||||
scheduledAt,
|
||||
groupedId,
|
||||
noWebPage,
|
||||
sendAs,
|
||||
shouldUpdateStickerSetOrder,
|
||||
wasDrafted,
|
||||
isInvertedMedia,
|
||||
effectId,
|
||||
webPageMediaSize,
|
||||
webPageUrl,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
lastMessageId?: number;
|
||||
text?: string;
|
||||
entities?: ApiMessageEntity[];
|
||||
replyInfo?: ApiInputReplyInfo;
|
||||
attachment?: ApiAttachment;
|
||||
sticker?: ApiSticker;
|
||||
story?: ApiStory | ApiStorySkipped;
|
||||
gif?: ApiVideo;
|
||||
poll?: ApiNewPoll;
|
||||
contact?: ApiContact;
|
||||
isSilent?: boolean;
|
||||
scheduledAt?: number;
|
||||
groupedId?: string;
|
||||
noWebPage?: boolean;
|
||||
sendAs?: ApiPeer;
|
||||
shouldUpdateStickerSetOrder?: boolean;
|
||||
wasDrafted?: boolean;
|
||||
isInvertedMedia?: true;
|
||||
effectId?: string;
|
||||
webPageMediaSize?: WebPageMediaSize;
|
||||
webPageUrl?: string;
|
||||
},
|
||||
onProgress?: ApiOnProgress,
|
||||
export function sendMessageLocal(
|
||||
params: SendMessageParams,
|
||||
) {
|
||||
const {
|
||||
chat, lastMessageId, text, entities, replyInfo, attachment, sticker, story, gif, poll, contact,
|
||||
scheduledAt, groupedId, sendAs, wasDrafted, isInvertedMedia, effectId, isPending, messagePriceInStars,
|
||||
} = params;
|
||||
|
||||
if (!chat) return undefined;
|
||||
|
||||
const {
|
||||
message: localMessage,
|
||||
poll: localPoll,
|
||||
@ -325,6 +283,8 @@ export function sendMessage(
|
||||
story,
|
||||
isInvertedMedia,
|
||||
effectId,
|
||||
isPending,
|
||||
messagePriceInStars,
|
||||
);
|
||||
|
||||
sendApiUpdate({
|
||||
@ -336,6 +296,22 @@ export function sendMessage(
|
||||
wasDrafted,
|
||||
});
|
||||
|
||||
return localMessage;
|
||||
}
|
||||
|
||||
export function sendApiMessage(
|
||||
params: SendMessageParams,
|
||||
localMessage: ApiMessage,
|
||||
onProgress?: ApiOnProgress,
|
||||
) {
|
||||
const {
|
||||
chat, text, entities, replyInfo, attachment, sticker, story, gif, poll, contact,
|
||||
isSilent, scheduledAt, groupedId, noWebPage, sendAs, shouldUpdateStickerSetOrder,
|
||||
isInvertedMedia, effectId, webPageMediaSize, webPageUrl, messagePriceInStars,
|
||||
} = params;
|
||||
|
||||
if (!chat) return undefined;
|
||||
|
||||
// This is expected to arrive after `updateMessageSendSucceeded` which replaces the local ID,
|
||||
// so in most cases this will be simply ignored
|
||||
const timeout = setTimeout(() => {
|
||||
@ -361,6 +337,7 @@ export function sendMessage(
|
||||
groupedId,
|
||||
isSilent,
|
||||
scheduledAt,
|
||||
messagePriceInStars,
|
||||
}, randomId, localMessage, onProgress);
|
||||
}
|
||||
|
||||
@ -420,6 +397,7 @@ export function sendMessage(
|
||||
...(shouldUpdateStickerSetOrder && { updateStickersetsOrder: shouldUpdateStickerSetOrder }),
|
||||
...(isInvertedMedia && { invertMedia: isInvertedMedia }),
|
||||
...(effectId && { effect: BigInt(effectId) }),
|
||||
...(messagePriceInStars && { allowPaidStars: BigInt(messagePriceInStars) }),
|
||||
}), {
|
||||
shouldThrow: true,
|
||||
shouldIgnoreUpdates: true,
|
||||
@ -443,6 +421,14 @@ export function sendMessage(
|
||||
return messagePromise;
|
||||
}
|
||||
|
||||
export function sendMessage(
|
||||
params: SendMessageParams,
|
||||
onProgress?: ApiOnProgress,
|
||||
) {
|
||||
const localMessage = params.localMessage || sendMessageLocal(params);
|
||||
return localMessage ? sendApiMessage(params, localMessage, onProgress) : undefined;
|
||||
}
|
||||
|
||||
const groupedUploads: Record<string, {
|
||||
counter: number;
|
||||
singleMediaByIndex: Record<number, GramJs.InputSingleMedia>;
|
||||
@ -460,6 +446,7 @@ function sendGroupedMedia(
|
||||
isSilent,
|
||||
scheduledAt,
|
||||
sendAs,
|
||||
messagePriceInStars,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
text?: string;
|
||||
@ -470,6 +457,7 @@ function sendGroupedMedia(
|
||||
isSilent?: boolean;
|
||||
scheduledAt?: number;
|
||||
sendAs?: ApiPeer;
|
||||
messagePriceInStars?: number;
|
||||
},
|
||||
randomId: GramJs.long,
|
||||
localMessage: ApiMessage,
|
||||
@ -536,6 +524,7 @@ function sendGroupedMedia(
|
||||
|
||||
const { singleMediaByIndex, localMessages } = groupedUploads[groupedId];
|
||||
delete groupedUploads[groupedId];
|
||||
const count = Object.values(singleMediaByIndex).length;
|
||||
|
||||
const update = await invokeRequest(new GramJs.messages.SendMultiMedia({
|
||||
clearDraft: true,
|
||||
@ -545,6 +534,7 @@ function sendGroupedMedia(
|
||||
...(isSilent && { silent: isSilent }),
|
||||
...(scheduledAt && { scheduleDate: scheduledAt }),
|
||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||
...(messagePriceInStars && { allowPaidStars: BigInt(messagePriceInStars * count) }),
|
||||
}), {
|
||||
shouldIgnoreUpdates: true,
|
||||
});
|
||||
@ -1534,40 +1524,17 @@ export async function fetchExtendedMedia({
|
||||
}));
|
||||
}
|
||||
|
||||
export async function forwardMessages({
|
||||
fromChat,
|
||||
toChat,
|
||||
toThreadId,
|
||||
messages,
|
||||
isSilent,
|
||||
scheduledAt,
|
||||
sendAs,
|
||||
withMyScore,
|
||||
noAuthors,
|
||||
noCaptions,
|
||||
isCurrentUserPremium,
|
||||
wasDrafted,
|
||||
lastMessageId,
|
||||
}: {
|
||||
fromChat: ApiChat;
|
||||
toChat: ApiChat;
|
||||
toThreadId?: ThreadId;
|
||||
messages: ApiMessage[];
|
||||
isSilent?: boolean;
|
||||
scheduledAt?: number;
|
||||
sendAs?: ApiPeer;
|
||||
withMyScore?: boolean;
|
||||
noAuthors?: boolean;
|
||||
noCaptions?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
wasDrafted?: boolean;
|
||||
lastMessageId?: number;
|
||||
}) {
|
||||
const messageIds = messages.map(({ id }) => id);
|
||||
const randomIds = messages.map(generateRandomBigInt);
|
||||
const localMessages: Record<string, ApiMessage> = {};
|
||||
export function forwardMessagesLocal(params: ForwardMessagesParams) {
|
||||
const {
|
||||
toChat, toThreadId, messages,
|
||||
scheduledAt, sendAs, noAuthors, noCaptions,
|
||||
isCurrentUserPremium, wasDrafted, lastMessageId,
|
||||
} = params;
|
||||
|
||||
messages.forEach((message, index) => {
|
||||
const messageIds = messages.map(({ id }) => id);
|
||||
const localMessages: ApiMessage[] = [];
|
||||
|
||||
messages.forEach((message) => {
|
||||
const localMessage = buildLocalForwardedMessage({
|
||||
toChat,
|
||||
toThreadId: Number(toThreadId),
|
||||
@ -1579,7 +1546,7 @@ export async function forwardMessages({
|
||||
lastMessageId,
|
||||
sendAs,
|
||||
});
|
||||
localMessages[randomIds[index].toString()] = localMessage;
|
||||
localMessages.push(localMessage);
|
||||
|
||||
sendApiUpdate({
|
||||
'@type': localMessage.isScheduled ? 'newScheduledMessage' : 'newMessage',
|
||||
@ -1589,7 +1556,25 @@ export async function forwardMessages({
|
||||
wasDrafted,
|
||||
});
|
||||
});
|
||||
return { messageIds, localMessages };
|
||||
}
|
||||
|
||||
export async function forwardApiMessages(params: ForwardMessagesParams) {
|
||||
const {
|
||||
fromChat, toChat, toThreadId, isSilent,
|
||||
scheduledAt, sendAs, withMyScore, noAuthors, noCaptions,
|
||||
forwardedLocalMessagesSlice, messagePriceInStars,
|
||||
} = params;
|
||||
|
||||
if (!forwardedLocalMessagesSlice) return;
|
||||
|
||||
const {
|
||||
messageIds, localMessages,
|
||||
} = forwardedLocalMessagesSlice;
|
||||
|
||||
const priceInStars = messagePriceInStars ? messagePriceInStars * messageIds.length : undefined;
|
||||
|
||||
const randomIds = messageIds.map(generateRandomBigInt);
|
||||
try {
|
||||
const update = await invokeRequest(new GramJs.messages.ForwardMessages({
|
||||
fromPeer: buildInputPeer(fromChat.id, fromChat.accessHash),
|
||||
@ -1603,11 +1588,16 @@ export async function forwardMessages({
|
||||
...(toThreadId && { topMsgId: Number(toThreadId) }),
|
||||
...(scheduledAt && { scheduleDate: scheduledAt }),
|
||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||
...(priceInStars && { allowPaidStars: BigInt(priceInStars) }),
|
||||
}), {
|
||||
shouldThrow: true,
|
||||
shouldIgnoreUpdates: true,
|
||||
});
|
||||
if (update) handleMultipleLocalMessagesUpdate(localMessages, update);
|
||||
const messagesForUpdate: Record<string, ApiMessage> = {};
|
||||
localMessages.forEach((message, index) => {
|
||||
messagesForUpdate[randomIds[index].toString()] = message;
|
||||
});
|
||||
if (update) handleMultipleLocalMessagesUpdate(messagesForUpdate, update);
|
||||
} catch (error: any) {
|
||||
Object.values(localMessages).forEach((localMessage) => {
|
||||
sendApiUpdate({
|
||||
@ -1620,6 +1610,18 @@ export async function forwardMessages({
|
||||
}
|
||||
}
|
||||
|
||||
export async function forwardMessages(params: ForwardMessagesParams) {
|
||||
if (params.forwardedLocalMessagesSlice) {
|
||||
await forwardApiMessages(params);
|
||||
} else {
|
||||
const newParams = {
|
||||
...params,
|
||||
forwardedLocalMessagesSlice: forwardMessagesLocal(params),
|
||||
};
|
||||
await forwardApiMessages(newParams);
|
||||
}
|
||||
}
|
||||
|
||||
export async function findFirstMessageIdAfterDate({
|
||||
chat,
|
||||
timestamp,
|
||||
|
||||
@ -656,6 +656,7 @@ export async function fetchGlobalPrivacySettings() {
|
||||
shouldArchiveAndMuteNewNonContact: Boolean(result.archiveAndMuteNewNoncontactPeers),
|
||||
shouldHideReadMarks: Boolean(result.hideReadMarks),
|
||||
shouldNewNonContactPeersRequirePremium: Boolean(result.newNoncontactPeersRequirePremium),
|
||||
nonContactPeersPaidStars: Number(result.noncontactPeersPaidStars),
|
||||
};
|
||||
}
|
||||
|
||||
@ -663,16 +664,19 @@ export async function updateGlobalPrivacySettings({
|
||||
shouldArchiveAndMuteNewNonContact,
|
||||
shouldHideReadMarks,
|
||||
shouldNewNonContactPeersRequirePremium,
|
||||
nonContactPeersPaidStars,
|
||||
}: {
|
||||
shouldArchiveAndMuteNewNonContact?: boolean;
|
||||
shouldHideReadMarks?: boolean;
|
||||
shouldNewNonContactPeersRequirePremium?: boolean;
|
||||
nonContactPeersPaidStars?: number | null;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.account.SetGlobalPrivacySettings({
|
||||
settings: new GramJs.GlobalPrivacySettings({
|
||||
...(shouldArchiveAndMuteNewNonContact && { archiveAndMuteNewNoncontactPeers: true }),
|
||||
...(shouldHideReadMarks && { hideReadMarks: true }),
|
||||
...(shouldNewNonContactPeersRequirePremium && { newNoncontactPeersRequirePremium: true }),
|
||||
noncontactPeersPaidStars: BigInt(nonContactPeersPaidStars || 0),
|
||||
}),
|
||||
}));
|
||||
|
||||
@ -684,6 +688,7 @@ export async function updateGlobalPrivacySettings({
|
||||
shouldArchiveAndMuteNewNonContact: Boolean(result.archiveAndMuteNewNoncontactPeers),
|
||||
shouldHideReadMarks: Boolean(result.hideReadMarks),
|
||||
shouldNewNonContactPeersRequirePremium: Boolean(result.newNoncontactPeersRequirePremium),
|
||||
nonContactPeersPaidStars: Number(result.noncontactPeersPaidStars),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -103,6 +103,22 @@ export async function fetchCommonChats(user: ApiUser, maxId?: string) {
|
||||
return { chatIds, count };
|
||||
}
|
||||
|
||||
export async function fetchPaidMessagesStarsAmount(user: ApiUser) {
|
||||
const result = await invokeRequest(new GramJs.users.GetRequirementsToContact({
|
||||
id: [buildInputEntity(user.id, user.accessHash) as GramJs.InputUser],
|
||||
}));
|
||||
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (result[0] instanceof GramJs.RequirementToContactPaidMessages) {
|
||||
return result[0].starsAmount?.toJSNumber();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function fetchNearestCountry() {
|
||||
const dcInfo = await invokeRequest(new GramJs.help.GetNearestDc());
|
||||
|
||||
@ -231,6 +247,17 @@ export async function deleteContact({
|
||||
});
|
||||
}
|
||||
|
||||
export async function addNoPaidMessagesException({ user, shouldRefundCharged }: {
|
||||
user: ApiUser;
|
||||
shouldRefundCharged?: boolean;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.account.AddNoPaidMessagesException({
|
||||
refundCharged: shouldRefundCharged ? true : undefined,
|
||||
userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser,
|
||||
}));
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function fetchProfilePhotos({
|
||||
peer,
|
||||
offset = 0,
|
||||
|
||||
@ -88,6 +88,8 @@ export interface ApiChat {
|
||||
|
||||
// Locally determined field
|
||||
detectedLanguage?: string;
|
||||
|
||||
paidMessagesStars?: number;
|
||||
}
|
||||
|
||||
export interface ApiTypingStatus {
|
||||
@ -232,6 +234,7 @@ export interface ApiPeerSettings {
|
||||
canReportSpam?: boolean;
|
||||
canAddContact?: boolean;
|
||||
canBlockContact?: boolean;
|
||||
chargedPaidMessageStars?: number;
|
||||
registrationMonth?: string;
|
||||
phoneCountry?: string;
|
||||
nameChangeDate?: number;
|
||||
|
||||
@ -585,6 +585,7 @@ export interface ApiMessage {
|
||||
isVideoProcessingPending?: true;
|
||||
areReactionsPossible?: true;
|
||||
reportDeliveryUntilDate?: number;
|
||||
paidMessageStars?: number;
|
||||
}
|
||||
|
||||
export interface ApiReactions {
|
||||
|
||||
@ -109,6 +109,7 @@ export interface ApiSessionData {
|
||||
export type ApiNotification = {
|
||||
localId: string;
|
||||
containerSelector?: string;
|
||||
type?: 'paidMessage' | undefined;
|
||||
title?: string | RegularLangFnParameters;
|
||||
message: TeactNode | RegularLangFnParameters;
|
||||
cacheBreaker?: string;
|
||||
@ -120,6 +121,7 @@ export type ApiNotification = {
|
||||
shouldShowTimer?: boolean;
|
||||
icon?: IconName;
|
||||
customEmojiIconId?: string;
|
||||
shouldUseCustomIcon?: boolean;
|
||||
dismissAction?: CallbackAction;
|
||||
};
|
||||
|
||||
@ -224,6 +226,10 @@ export interface ApiAppConfig {
|
||||
maxPinnedStoriesCount?: number;
|
||||
groupTranscribeLevelMin?: number;
|
||||
canLimitNewMessagesWithoutPremium?: boolean;
|
||||
starsPaidMessagesAvailable?: boolean;
|
||||
starsPaidMessageCommissionPermille?: number;
|
||||
starsPaidMessageAmountMax?: number;
|
||||
starsUsdWithdrawRateX1000?: number;
|
||||
bandwidthPremiumNotifyPeriod?: number;
|
||||
bandwidthPremiumUploadSpeedup?: number;
|
||||
bandwidthPremiumDownloadSpeedup?: number;
|
||||
|
||||
@ -2,7 +2,7 @@ import type { ApiChat } from './chats';
|
||||
import type { ApiUser } from './users';
|
||||
|
||||
export type ApiPrivacyKey = 'phoneNumber' | 'addByPhone' | 'lastSeen' | 'profilePhoto' | 'voiceMessages' |
|
||||
'forwards' | 'chatInvite' | 'phoneCall' | 'phoneP2P' | 'bio' | 'birthday' | 'gifts';
|
||||
'forwards' | 'chatInvite' | 'phoneCall' | 'phoneP2P' | 'bio' | 'birthday' | 'gifts' | 'noPaidMessages';
|
||||
export type PrivacyVisibility = 'everybody' | 'contacts' | 'closeFriends' | 'nonContacts' | 'nobody';
|
||||
export type BotsPrivacyType = 'allow' | 'disallow' | 'none';
|
||||
|
||||
|
||||
@ -180,6 +180,7 @@ export interface ApiStarsTransaction {
|
||||
subscriptionPeriod?: number;
|
||||
starRefCommision?: number;
|
||||
isGiftUpgrade?: true;
|
||||
paidMessages?: number;
|
||||
}
|
||||
|
||||
export interface ApiStarsSubscription {
|
||||
|
||||
@ -38,6 +38,7 @@ export interface ApiUser {
|
||||
hasMainMiniApp?: boolean;
|
||||
botActiveUsers?: number;
|
||||
botVerificationIconId?: string;
|
||||
paidMessagesStars?: number;
|
||||
}
|
||||
|
||||
export interface ApiUserFullInfo {
|
||||
@ -65,6 +66,7 @@ export interface ApiUserFullInfo {
|
||||
isBotAccessEmojiGranted?: boolean;
|
||||
hasScheduledMessages?: boolean;
|
||||
botVerification?: ApiBotVerification;
|
||||
paidMessagesStars?: number;
|
||||
settings?: ApiPeerSettings;
|
||||
}
|
||||
|
||||
|
||||
@ -1853,3 +1853,41 @@
|
||||
"GiftPremiumDescriptionYourBalance" = "Your balance is **{stars}**. {link}";
|
||||
"StarsGiftCompleted"= "Gift sent!";
|
||||
"GiftSent"= "Gift sent!";
|
||||
"PrivacyDescriptionMessagesContactsAndPremium" = "You can restrict messages from users who are not in your contacts and don't have Premium.";
|
||||
"PrivacyChargeForMessages" = "Charge for Messages";
|
||||
"PrivacyDescriptionChargeForMessages" = "Charge a fee for messages from people outside your contacts or those you haven't messaged first.";
|
||||
"RemoveFeeTitle" = "Remove Fee";
|
||||
"ExceptionTitlePrivacyChargeForMessages" = "Remove fee";
|
||||
"ExceptionDescriptionPrivacyChargeForMessages" = "Add users or entire groups who won't be charged for sending messages to you.";
|
||||
"SectionTitleStarsForForMessages" = "Set your price per message";
|
||||
"SectionDescriptionStarsForForMessages" = "You will receive {percent}% of the selected fee (~{amount}) for each incoming message.";
|
||||
"SubtitlePrivacyAddUsers" = "Add Users";
|
||||
"SubtitlePrivacyUsersCount" = "{count} users";
|
||||
"PrivacyPaidMessagesValue" = "Paid";
|
||||
"FirstMessageInPaidMessagesChat" = "**{user}** charges {amount} for each message.";
|
||||
"ButtonBuyStars" = "Buy Stars";
|
||||
"ComposerPlaceholderPaidMessage" = "Message for {amount}";
|
||||
"ComposerPlaceholderPaidReply" = "Reply for {amount}";
|
||||
"TitleConfirmPayment" = "Confirm Payment";
|
||||
"ConfirmationModalPaymentForOneMessage" = "{user} charges **{amount} Stars** per incoming message. Would you like to pay **{amount} Stars** to send one message?";
|
||||
"ConfirmationModalPaymentForMessages" = "{user} charges **{price} Stars** per incoming message. Would you like to pay **{amount} Stars** to send **{count} messages?**";
|
||||
"ButtonPayForMessage" = "Pay for {count} message";
|
||||
"ToastTitleMessageSent" = "Message sent!";
|
||||
"ToastTitleMessagesSent" = "{count} Messages sent!";
|
||||
"ToastMessageSent" = "You paid {amount} stars.";
|
||||
"ButtonUndo" = "Undo";
|
||||
"ActionPaidOneMessageOutgoing" = "You paid {amount} Stars to send a message";
|
||||
"ActionPaidOneMessageIncoming" = "You received {amount} Stars from {user}";
|
||||
"PaneMessagePaidMessageCharge" = "{peer} must pay {amount} for each message to you.";
|
||||
"ConfirmRemoveMessageFee" = "Yes";
|
||||
"ConfirmDialogMessageRemoveFee" = "Are you sure you want to allow **{peer}** to message you for free?";
|
||||
"ConfirmDialogRemoveFeeRefundStars" = "Refund already paid **{amount} Stars**";
|
||||
"DescriptionGiftPaidMessage" = "{user} charges **{amount}** Stars for each message. That price has been added to the cost of the gift.";
|
||||
"StoryTooltipGifSent" = "Gif Sent!";
|
||||
"StoryTooltipStickerSent" = "Sticker Sent!";
|
||||
"StoryTooltipReactionSent" = "Reaction Sent!";
|
||||
"StarsNeededTextSendPaidMessages" = "Buy **Stars** to send messages.";
|
||||
"PaidMessageTransaction_one" = "Fee for {count} Message";
|
||||
"PaidMessageTransaction_other" = "Fee for {count} Messages";
|
||||
"PaidMessageTransactionDescription" = "You receive **{percent}** of the price that you charge for each incoming message.";
|
||||
"PaidMessageTransactionTotal" = "Total";
|
||||
@ -72,6 +72,7 @@
|
||||
}
|
||||
|
||||
> .Button {
|
||||
overflow: visible;
|
||||
flex-shrink: 0;
|
||||
margin-left: 0.5rem;
|
||||
width: var(--base-height);
|
||||
@ -96,6 +97,15 @@
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.paidStarsBadgeText {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
.star-amount-icon {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
&:not(:active):not(:focus):not(:hover) {
|
||||
.icon-send,
|
||||
@ -152,6 +162,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
.paidStarsBadgeIcon {
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: 0.0625rem;
|
||||
}
|
||||
|
||||
.paidStarsBadge {
|
||||
animation: hide-icon 0.4s forwards ease-out;
|
||||
&.visible {
|
||||
animation: grow-icon 0.4s ease-out;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
position: absolute;
|
||||
|
||||
top: -1rem;
|
||||
height: auto;
|
||||
padding-inline: 0.375rem;
|
||||
padding-block: 0.25rem;
|
||||
font-size: 0.8125rem;
|
||||
margin-top: 0.625rem;
|
||||
line-height: 1;
|
||||
font-weight: var(--font-weight-semibold) !important;
|
||||
}
|
||||
|
||||
&.send, &.sendOneTime {
|
||||
.icon-send {
|
||||
animation: grow-icon 0.4s ease-out;
|
||||
@ -652,8 +689,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder-star-icon {
|
||||
line-height: 1;
|
||||
}
|
||||
.forced-placeholder,
|
||||
.placeholder-text {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
color: var(--color-placeholders);
|
||||
pointer-events: none;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type { FC, TeactNode } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useMemo, useRef, useSignal, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
@ -57,6 +57,7 @@ import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterd
|
||||
import {
|
||||
canEditMedia,
|
||||
getAllowedAttachmentOptions,
|
||||
getPeerTitle,
|
||||
getReactionKey,
|
||||
getStoryKey,
|
||||
isChatAdmin,
|
||||
@ -90,6 +91,7 @@ import {
|
||||
selectNotifyDefaults,
|
||||
selectNotifyException,
|
||||
selectNoWebPage,
|
||||
selectPeerPaidMessagesStars,
|
||||
selectPeerStory,
|
||||
selectPerformanceSettingsValue,
|
||||
selectRequestedDraft,
|
||||
@ -108,6 +110,7 @@ import { tryParseDeepLink } from '../../util/deepLinkParser';
|
||||
import deleteLastCharacterOutsideSelection from '../../util/deleteLastCharacterOutsideSelection';
|
||||
import { processMessageInputForCustomEmoji } from '../../util/emoji/customEmojiManager';
|
||||
import focusEditableElement from '../../util/focusEditableElement';
|
||||
import { formatStarsAsIcon } from '../../util/localization/format';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||
import parseHtmlAsFormattedText from '../../util/parseHtmlAsFormattedText';
|
||||
import { insertHtmlInSelection } from '../../util/selection';
|
||||
@ -147,6 +150,7 @@ import useEditing from '../middle/composer/hooks/useEditing';
|
||||
import useEmojiTooltip from '../middle/composer/hooks/useEmojiTooltip';
|
||||
import useInlineBotTooltip from '../middle/composer/hooks/useInlineBotTooltip';
|
||||
import useMentionTooltip from '../middle/composer/hooks/useMentionTooltip';
|
||||
import usePaidMessageConfirmation from '../middle/composer/hooks/usePaidMessageConfirmation';
|
||||
import useStickerTooltip from '../middle/composer/hooks/useStickerTooltip';
|
||||
import useVoiceRecording from '../middle/composer/hooks/useVoiceRecording';
|
||||
|
||||
@ -175,8 +179,10 @@ import Button from '../ui/Button';
|
||||
import ResponsiveHoverButton from '../ui/ResponsiveHoverButton';
|
||||
import Spinner from '../ui/Spinner';
|
||||
import Transition from '../ui/Transition';
|
||||
import AnimatedCounter from './AnimatedCounter';
|
||||
import Avatar from './Avatar';
|
||||
import Icon from './icons/Icon';
|
||||
import PaymentMessageConfirmDialog from './PaymentMessageConfirmDialog';
|
||||
import ReactionAnimatedEmoji from './reactions/ReactionAnimatedEmoji';
|
||||
|
||||
import './Composer.scss';
|
||||
@ -196,7 +202,7 @@ type OwnProps = {
|
||||
editableInputCssSelector: string;
|
||||
editableInputId: string;
|
||||
className?: string;
|
||||
inputPlaceholder?: string;
|
||||
inputPlaceholder?: TeactNode | string;
|
||||
onDropHide?: NoneToVoidFunction;
|
||||
onForward?: NoneToVoidFunction;
|
||||
onFocus?: NoneToVoidFunction;
|
||||
@ -220,6 +226,7 @@ type StateProps =
|
||||
isSelectModeActive?: boolean;
|
||||
isReactionPickerOpen?: boolean;
|
||||
isForwarding?: boolean;
|
||||
forwardedMessagesCount?: number;
|
||||
pollModal: TabState['pollModal'];
|
||||
botKeyboardMessageId?: number;
|
||||
botKeyboardPlaceholder?: string;
|
||||
@ -271,13 +278,16 @@ type StateProps =
|
||||
webPagePreview?: ApiWebPage;
|
||||
noWebPage?: boolean;
|
||||
isContactRequirePremium?: boolean;
|
||||
paidMessagesStars?: number;
|
||||
effect?: ApiAvailableEffect;
|
||||
effectReactions?: ApiReaction[];
|
||||
areEffectsSupported?: boolean;
|
||||
canPlayEffect?: boolean;
|
||||
shouldPlayEffect?: boolean;
|
||||
maxMessageLength: number;
|
||||
shouldPaidMessageAutoApprove?: boolean;
|
||||
isSilentPosting?: boolean;
|
||||
isPaymentMessageConfirmDialogOpen: boolean;
|
||||
};
|
||||
|
||||
enum MainButtonState {
|
||||
@ -331,6 +341,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
isSelectModeActive,
|
||||
isReactionPickerOpen,
|
||||
isForwarding,
|
||||
forwardedMessagesCount,
|
||||
pollModal,
|
||||
botKeyboardMessageId,
|
||||
botKeyboardPlaceholder,
|
||||
@ -382,6 +393,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
webPagePreview,
|
||||
noWebPage,
|
||||
isContactRequirePremium,
|
||||
paidMessagesStars,
|
||||
effect,
|
||||
effectReactions,
|
||||
areEffectsSupported,
|
||||
@ -393,12 +405,12 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
onFocus,
|
||||
onBlur,
|
||||
onForward,
|
||||
isPaymentMessageConfirmDialogOpen,
|
||||
}) => {
|
||||
const {
|
||||
sendMessage,
|
||||
clearDraft,
|
||||
showDialog,
|
||||
forwardMessages,
|
||||
openPollModal,
|
||||
closePollModal,
|
||||
loadScheduledHistory,
|
||||
@ -427,6 +439,8 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const inputRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const counterRef = useRef<HTMLSpanElement>(null);
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const storyReactionRef = useRef<HTMLButtonElement>(null);
|
||||
@ -513,6 +527,22 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
const isNeedPremium = isContactRequirePremium && isInStoryViewer;
|
||||
const isSendTextBlocked = isNeedPremium || !canSendPlainText;
|
||||
|
||||
const messagesCount = useDerivedState(() => {
|
||||
if (hasAttachments) return attachments.length;
|
||||
const messagesInInput = (getHtml() || hasAttachments) ? 1 : 0;
|
||||
if (!isForwarding || !forwardedMessagesCount) return messagesInInput || 1;
|
||||
return forwardedMessagesCount + messagesInInput;
|
||||
}, [getHtml, hasAttachments, attachments, isForwarding, forwardedMessagesCount]);
|
||||
const starsForAllMessages = paidMessagesStars ? messagesCount * paidMessagesStars : 0;
|
||||
|
||||
const {
|
||||
closeConfirmDialog: closeConfirmModalPayForMessage,
|
||||
dialogHandler: paymentMessageConfirmDialogHandler,
|
||||
shouldAutoApprove: shouldPaidMessageAutoApprove,
|
||||
setAutoApprove: setShouldPaidMessageAutoApprove,
|
||||
handleWithConfirmation: handleActionWithPaymentConfirmation,
|
||||
} = usePaidMessageConfirmation(starsForAllMessages);
|
||||
|
||||
const hasWebPagePreview = !hasAttachments && canAttachEmbedLinks && !noWebPage && Boolean(webPagePreview);
|
||||
const isComposerBlocked = isSendTextBlocked && !editingMessage;
|
||||
|
||||
@ -933,6 +963,21 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
return true;
|
||||
});
|
||||
|
||||
const canSendAttachments = (attachmentsToSend: ApiAttachment[]): boolean => {
|
||||
if (!currentMessageList && !storyId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { text } = parseHtmlAsFormattedText(getHtml());
|
||||
if (!text && !attachmentsToSend.length) {
|
||||
return false;
|
||||
}
|
||||
if (!validateTextLength(text, true)) return false;
|
||||
if (!checkSlowMode()) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const sendAttachments = useLastCallback(({
|
||||
attachments: attachmentsToSend,
|
||||
sendCompressed = attachmentSettings.shouldCompress,
|
||||
@ -954,11 +999,6 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
isSilent = isSilent || isSilentPosting;
|
||||
|
||||
const { text, entities } = parseHtmlAsFormattedText(getHtml());
|
||||
if (!text && !attachmentsToSend.length) {
|
||||
return;
|
||||
}
|
||||
if (!validateTextLength(text, true)) return;
|
||||
if (!checkSlowMode()) return;
|
||||
|
||||
isInvertedMedia = text && sendCompressed && sendGrouped ? isInvertedMedia : undefined;
|
||||
|
||||
@ -998,12 +1038,24 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
sendGrouped: boolean,
|
||||
isInvertedMedia?: true,
|
||||
) => {
|
||||
sendAttachments({
|
||||
attachments,
|
||||
sendCompressed,
|
||||
sendGrouped,
|
||||
isInvertedMedia,
|
||||
});
|
||||
if (canSendAttachments(attachments)) {
|
||||
if (editingMessage) {
|
||||
sendAttachments({
|
||||
attachments,
|
||||
sendCompressed,
|
||||
sendGrouped,
|
||||
isInvertedMedia,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
handleActionWithPaymentConfirmation(sendAttachments, {
|
||||
attachments,
|
||||
sendCompressed,
|
||||
sendGrouped,
|
||||
isInvertedMedia,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const handleSendAttachments = useLastCallback((
|
||||
@ -1013,16 +1065,81 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
scheduledAt?: number,
|
||||
isInvertedMedia?: true,
|
||||
) => {
|
||||
sendAttachments({
|
||||
attachments,
|
||||
sendCompressed,
|
||||
sendGrouped,
|
||||
isSilent,
|
||||
scheduledAt,
|
||||
isInvertedMedia,
|
||||
});
|
||||
if (canSendAttachments(attachments)) {
|
||||
sendAttachments({
|
||||
attachments,
|
||||
sendCompressed,
|
||||
sendGrouped,
|
||||
isSilent,
|
||||
scheduledAt,
|
||||
isInvertedMedia,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const handleSendCore = useLastCallback(
|
||||
(currentAttachments: ApiAttachment[], isSilent = false, scheduledAt?: number) => {
|
||||
const { text, entities } = parseHtmlAsFormattedText(getHtml());
|
||||
|
||||
if (currentAttachments.length) {
|
||||
if (canSendAttachments(currentAttachments)) {
|
||||
sendAttachments({
|
||||
attachments: currentAttachments,
|
||||
scheduledAt,
|
||||
isSilent,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!text && !isForwarding) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateTextLength(text)) return;
|
||||
|
||||
const messageInput = document.querySelector<HTMLDivElement>(editableInputCssSelector);
|
||||
|
||||
const effectId = effect?.id;
|
||||
|
||||
if (text || isForwarding) {
|
||||
if (!checkSlowMode()) return;
|
||||
|
||||
const isInvertedMedia = hasWebPagePreview ? attachmentSettings.isInvertedMedia : undefined;
|
||||
|
||||
if (areEffectsSupported) saveEffectInDraft({ chatId, threadId, effectId: undefined });
|
||||
|
||||
sendMessage({
|
||||
messageList: currentMessageList,
|
||||
text,
|
||||
entities,
|
||||
scheduledAt,
|
||||
isSilent,
|
||||
shouldUpdateStickerSetOrder,
|
||||
isInvertedMedia,
|
||||
effectId,
|
||||
webPageMediaSize: attachmentSettings.webPageMediaSize,
|
||||
webPageUrl: hasWebPagePreview ? webPagePreview!.url : undefined,
|
||||
isForwarding,
|
||||
});
|
||||
}
|
||||
|
||||
lastMessageSendTimeSeconds.current = getServerTime();
|
||||
clearDraft({
|
||||
chatId, threadId, isLocalOnly: true, shouldKeepReply: isForwarding,
|
||||
});
|
||||
|
||||
if (IS_IOS && messageInput && messageInput === document.activeElement) {
|
||||
applyIosAutoCapitalizationFix(messageInput);
|
||||
}
|
||||
|
||||
// Wait until message animation starts
|
||||
requestMeasure(() => {
|
||||
resetComposer();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const handleSend = useLastCallback(async (isSilent = false, scheduledAt?: number) => {
|
||||
if (!currentMessageList && !storyId) {
|
||||
return;
|
||||
@ -1045,68 +1162,11 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const { text, entities } = parseHtmlAsFormattedText(getHtml());
|
||||
handleSendCore(currentAttachments, isSilent, scheduledAt);
|
||||
});
|
||||
|
||||
if (currentAttachments.length) {
|
||||
sendAttachments({
|
||||
attachments: currentAttachments,
|
||||
scheduledAt,
|
||||
isSilent,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!text && !isForwarding) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateTextLength(text)) return;
|
||||
|
||||
const messageInput = document.querySelector<HTMLDivElement>(editableInputCssSelector);
|
||||
|
||||
const effectId = effect?.id;
|
||||
|
||||
if (text) {
|
||||
if (!checkSlowMode()) return;
|
||||
|
||||
const isInvertedMedia = hasWebPagePreview ? attachmentSettings.isInvertedMedia : undefined;
|
||||
|
||||
if (areEffectsSupported) saveEffectInDraft({ chatId, threadId, effectId: undefined });
|
||||
|
||||
sendMessage({
|
||||
messageList: currentMessageList,
|
||||
text,
|
||||
entities,
|
||||
scheduledAt,
|
||||
isSilent,
|
||||
shouldUpdateStickerSetOrder,
|
||||
isInvertedMedia,
|
||||
effectId,
|
||||
webPageMediaSize: attachmentSettings.webPageMediaSize,
|
||||
webPageUrl: hasWebPagePreview ? webPagePreview!.url : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
if (isForwarding) {
|
||||
forwardMessages({
|
||||
scheduledAt,
|
||||
isSilent,
|
||||
});
|
||||
}
|
||||
|
||||
lastMessageSendTimeSeconds.current = getServerTime();
|
||||
clearDraft({
|
||||
chatId, threadId, isLocalOnly: true, shouldKeepReply: isForwarding,
|
||||
});
|
||||
|
||||
if (IS_IOS && messageInput && messageInput === document.activeElement) {
|
||||
applyIosAutoCapitalizationFix(messageInput);
|
||||
}
|
||||
|
||||
// Wait until message animation starts
|
||||
requestMeasure(() => {
|
||||
resetComposer();
|
||||
});
|
||||
const handleSendWithConfirmation = useLastCallback((isSilent = false, scheduledAt?: number) => {
|
||||
handleActionWithPaymentConfirmation(handleSend, isSilent, scheduledAt);
|
||||
});
|
||||
|
||||
const handleClickBotMenu = useLastCallback(() => {
|
||||
@ -1215,13 +1275,13 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
forceShowSymbolMenu();
|
||||
requestCalendar((scheduledAt) => {
|
||||
cancelForceShowSymbolMenu();
|
||||
handleMessageSchedule({ gif, isSilent }, scheduledAt, currentMessageList!);
|
||||
handleActionWithPaymentConfirmation(handleMessageSchedule, { gif, isSilent }, scheduledAt, currentMessageList!);
|
||||
requestMeasure(() => {
|
||||
resetComposer(true);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
sendMessage({ messageList: currentMessageList, gif, isSilent });
|
||||
handleActionWithPaymentConfirmation(sendMessage, { messageList: currentMessageList, gif, isSilent });
|
||||
requestMeasure(() => {
|
||||
resetComposer(true);
|
||||
});
|
||||
@ -1250,18 +1310,23 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
forceShowSymbolMenu();
|
||||
requestCalendar((scheduledAt) => {
|
||||
cancelForceShowSymbolMenu();
|
||||
handleMessageSchedule({ sticker, isSilent }, scheduledAt, currentMessageList!);
|
||||
handleActionWithPaymentConfirmation(
|
||||
handleMessageSchedule, { sticker, isSilent }, scheduledAt, currentMessageList!,
|
||||
);
|
||||
requestMeasure(() => {
|
||||
resetComposer(shouldPreserveInput);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
sendMessage({
|
||||
messageList: currentMessageList,
|
||||
sticker,
|
||||
isSilent,
|
||||
shouldUpdateStickerSetOrder: shouldUpdateStickerSetOrder && canUpdateStickerSetsOrder,
|
||||
});
|
||||
handleActionWithPaymentConfirmation(
|
||||
sendMessage,
|
||||
{
|
||||
messageList: currentMessageList,
|
||||
sticker,
|
||||
isSilent,
|
||||
shouldUpdateStickerSetOrder: shouldUpdateStickerSetOrder && canUpdateStickerSetsOrder,
|
||||
},
|
||||
);
|
||||
clearDraft({ chatId, threadId, isLocalOnly: true });
|
||||
|
||||
requestMeasure(() => {
|
||||
@ -1281,20 +1346,24 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
|
||||
if (isInScheduledList || isScheduleRequested) {
|
||||
requestCalendar((scheduledAt) => {
|
||||
handleMessageSchedule({
|
||||
id: inlineResult.id,
|
||||
queryId: inlineResult.queryId,
|
||||
isSilent,
|
||||
}, scheduledAt, currentMessageList!);
|
||||
handleActionWithPaymentConfirmation(handleMessageSchedule,
|
||||
{
|
||||
id: inlineResult.id,
|
||||
queryId: inlineResult.queryId,
|
||||
isSilent,
|
||||
},
|
||||
scheduledAt,
|
||||
currentMessageList!);
|
||||
});
|
||||
} else {
|
||||
sendInlineBotResult({
|
||||
id: inlineResult.id,
|
||||
queryId: inlineResult.queryId,
|
||||
threadId,
|
||||
chatId,
|
||||
isSilent,
|
||||
});
|
||||
handleActionWithPaymentConfirmation(sendInlineBotResult,
|
||||
{
|
||||
id: inlineResult.id,
|
||||
queryId: inlineResult.queryId,
|
||||
threadId,
|
||||
chatId,
|
||||
isSilent,
|
||||
});
|
||||
}
|
||||
|
||||
const messageInput = document.querySelector<HTMLDivElement>(editableInputCssSelector);
|
||||
@ -1331,6 +1400,10 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
});
|
||||
|
||||
const handlePollSendWithPaymentConfirmation = useLastCallback((poll: ApiNewPoll) => {
|
||||
handleActionWithPaymentConfirmation(handlePollSend, poll);
|
||||
});
|
||||
|
||||
const sendSilent = useLastCallback((additionalArgs?: ScheduledMessageArgs) => {
|
||||
if (isInScheduledList) {
|
||||
requestCalendar((scheduledAt) => {
|
||||
@ -1458,6 +1531,13 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
if (!isComposerBlocked) {
|
||||
if (botKeyboardPlaceholder) return botKeyboardPlaceholder;
|
||||
if (inputPlaceholder) return inputPlaceholder;
|
||||
if (paidMessagesStars) {
|
||||
return lang('ComposerPlaceholderPaidMessage', {
|
||||
amount: formatStarsAsIcon(lang, paidMessagesStars, { asFont: true, className: 'placeholder-star-icon' }),
|
||||
}, {
|
||||
withNodes: true,
|
||||
});
|
||||
}
|
||||
if (chat?.isForum && chat?.isForumAsMessages && threadId === MAIN_THREAD_ID) {
|
||||
return replyToTopic
|
||||
? lang('ComposerPlaceholderTopic', { topic: replyToTopic.title })
|
||||
@ -1474,7 +1554,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
return lang('ComposerPlaceholderNoText');
|
||||
}, [
|
||||
activeVoiceRecording, botKeyboardPlaceholder, chat, inputPlaceholder, isChannel, isComposerBlocked,
|
||||
isInStoryViewer, isSilentPosting, lang, replyToTopic, threadId, windowWidth,
|
||||
isInStoryViewer, isSilentPosting, lang, replyToTopic, threadId, windowWidth, paidMessagesStars,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -1498,7 +1578,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
onForward?.();
|
||||
break;
|
||||
case MainButtonState.Send:
|
||||
void handleSend();
|
||||
handleSendWithConfirmation();
|
||||
break;
|
||||
case MainButtonState.Record: {
|
||||
if (areVoiceMessagesNotAllowed) {
|
||||
@ -1586,7 +1666,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
entities = customEmojiMessage.entities;
|
||||
}
|
||||
|
||||
sendMessage({ text, entities, isReaction: true });
|
||||
handleActionWithPaymentConfirmation(sendMessage, { text, entities, isReaction: true });
|
||||
closeReactionPicker();
|
||||
});
|
||||
|
||||
@ -1622,24 +1702,29 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
const handleSendSilent = useLastCallback(() => {
|
||||
sendSilent();
|
||||
handleActionWithPaymentConfirmation(sendSilent);
|
||||
});
|
||||
|
||||
const handleSendWhenOnline = useLastCallback(() => {
|
||||
handleMessageSchedule({}, SCHEDULED_WHEN_ONLINE, currentMessageList!, effect?.id);
|
||||
handleActionWithPaymentConfirmation(
|
||||
handleMessageSchedule, {}, SCHEDULED_WHEN_ONLINE, currentMessageList!, effect?.id,
|
||||
);
|
||||
});
|
||||
|
||||
const handleSendScheduledAttachments = useLastCallback(
|
||||
(sendCompressed: boolean, sendGrouped: boolean, isInvertedMedia?: true) => {
|
||||
requestCalendar((scheduledAt) => {
|
||||
handleMessageSchedule({ sendCompressed, sendGrouped, isInvertedMedia }, scheduledAt, currentMessageList!);
|
||||
handleActionWithPaymentConfirmation(handleMessageSchedule,
|
||||
{ sendCompressed, sendGrouped, isInvertedMedia },
|
||||
scheduledAt,
|
||||
currentMessageList!);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const handleSendSilentAttachments = useLastCallback(
|
||||
(sendCompressed: boolean, sendGrouped: boolean, isInvertedMedia?: true) => {
|
||||
sendSilent({ sendCompressed, sendGrouped, isInvertedMedia });
|
||||
handleActionWithPaymentConfirmation(sendSilent, { sendCompressed, sendGrouped, isInvertedMedia });
|
||||
},
|
||||
);
|
||||
|
||||
@ -1654,15 +1739,17 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
case MainButtonState.Schedule:
|
||||
return handleSendScheduled;
|
||||
default:
|
||||
return handleSend;
|
||||
return handleSendWithConfirmation;
|
||||
}
|
||||
}, [mainButtonState, handleEditComplete]);
|
||||
}, [mainButtonState, handleEditComplete, handleSendWithConfirmation]);
|
||||
|
||||
const withBotCommands = isChatWithBot && botMenuButton?.type === 'commands' && !editingMessage
|
||||
&& botCommands !== false && !activeVoiceRecording;
|
||||
|
||||
const effectEmoji = areEffectsSupported && effect?.emoticon;
|
||||
|
||||
const shouldRenderPaidBadge = Boolean(paidMessagesStars && mainButtonState === MainButtonState.Send);
|
||||
|
||||
return (
|
||||
<div className={fullClassName}>
|
||||
{isInMessageList && canAttachMedia && isReady && (
|
||||
@ -1702,7 +1789,8 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
shouldForceAsFile={shouldForceAsFile}
|
||||
isForCurrentMessageList={isForCurrentMessageList}
|
||||
isForMessage={isInMessageList}
|
||||
shouldSchedule={isInScheduledList}
|
||||
shouldSchedule={!paidMessagesStars && isInScheduledList}
|
||||
canSchedule={!paidMessagesStars}
|
||||
forceDarkTheme={isInStoryViewer}
|
||||
onCaptionUpdate={onCaptionUpdate}
|
||||
onSendSilent={handleSendSilentAttachments}
|
||||
@ -1717,13 +1805,14 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
editingMessage={editingMessage}
|
||||
onSendWhenOnline={handleSendWhenOnline}
|
||||
canScheduleUntilOnline={canScheduleUntilOnline && !isViewOnceEnabled}
|
||||
paidMessagesStars={paidMessagesStars}
|
||||
/>
|
||||
<PollModal
|
||||
isOpen={pollModal.isOpen}
|
||||
isQuiz={pollModal.isQuiz}
|
||||
shouldBeAnonymous={isChannel}
|
||||
onClear={closePollModal}
|
||||
onSend={handlePollSend}
|
||||
onSend={handlePollSendWithPaymentConfirmation}
|
||||
/>
|
||||
<SendAsMenu
|
||||
isOpen={isSendAsMenuOpen}
|
||||
@ -2109,6 +2198,22 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
{onForward && <Icon name="forward" />}
|
||||
{isInMessageList && <Icon name="schedule" />}
|
||||
{isInMessageList && <Icon name="check" />}
|
||||
<Button
|
||||
className={buildClassName('paidStarsBadge', shouldRenderPaidBadge && 'visible')}
|
||||
nonInteractive
|
||||
size="tiny"
|
||||
color="stars"
|
||||
pill
|
||||
fluid
|
||||
>
|
||||
<div className="paidStarsBadgeText">
|
||||
<Icon name="star" className={buildClassName('star-amount-icon', className)} />
|
||||
<AnimatedCounter
|
||||
ref={counterRef}
|
||||
text={lang.number(starsForAllMessages)}
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</Button>
|
||||
{effectEmoji && (
|
||||
<span className="effect-icon" onClick={handleRemoveEffect}>
|
||||
@ -2125,7 +2230,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
{canShowCustomSendMenu && (
|
||||
<CustomSendMenu
|
||||
isOpen={isCustomSendMenuOpen}
|
||||
canSchedule={isInMessageList && !isViewOnceEnabled}
|
||||
canSchedule={!paidMessagesStars && isInMessageList && !isViewOnceEnabled}
|
||||
canScheduleUntilOnline={canScheduleUntilOnline && !isViewOnceEnabled}
|
||||
onSendSilent={!isChatWithSelf ? handleSendSilent : undefined}
|
||||
onSendSchedule={!isInScheduledList ? handleSendScheduled : undefined}
|
||||
@ -2147,6 +2252,16 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
/>
|
||||
)}
|
||||
{calendar}
|
||||
<PaymentMessageConfirmDialog
|
||||
isOpen={isPaymentMessageConfirmDialogOpen}
|
||||
onClose={closeConfirmModalPayForMessage}
|
||||
userName={chat ? getPeerTitle(lang, chat) : undefined}
|
||||
messagePriceInStars={paidMessagesStars || 0}
|
||||
messagesCount={messagesCount}
|
||||
shouldAutoApprove={shouldPaidMessageAutoApprove}
|
||||
setAutoApprove={setShouldPaidMessageAutoApprove}
|
||||
confirmHandler={paymentMessageConfirmDialogHandler}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -2161,12 +2276,18 @@ export default memo(withGlobal<OwnProps>(
|
||||
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
|
||||
const isChatWithUser = isUserId(chatId);
|
||||
const userFullInfo = isChatWithUser ? selectUserFullInfo(global, chatId) : undefined;
|
||||
const paidMessagesStars = selectPeerPaidMessagesStars(global, chatId);
|
||||
|
||||
const chatFullInfo = !isChatWithUser ? selectChatFullInfo(global, chatId) : undefined;
|
||||
const messageWithActualBotKeyboard = (isChatWithBot || !isChatWithUser)
|
||||
&& selectNewestMessageWithBotKeyboardButtons(global, chatId, threadId);
|
||||
const {
|
||||
language, shouldSuggestStickers, shouldSuggestCustomEmoji, shouldUpdateStickerSetOrder,
|
||||
shouldPaidMessageAutoApprove,
|
||||
} = global.settings.byKey;
|
||||
const {
|
||||
forwardMessages: { messageIds: forwardMessageIds },
|
||||
} = selectTabState(global);
|
||||
const baseEmojiKeywords = global.emojiKeywords[BASE_EMOJI_KEYWORD_LANG];
|
||||
const emojiKeywords = language !== BASE_EMOJI_KEYWORD_LANG ? global.emojiKeywords[language] : undefined;
|
||||
const botKeyboardMessageId = messageWithActualBotKeyboard ? messageWithActualBotKeyboard.id : undefined;
|
||||
@ -2230,6 +2351,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const effectReactions = global.reactions.effectReactions;
|
||||
|
||||
const maxMessageLength = global.config?.maxMessageLength || DEFAULT_MAX_MESSAGE_LENGTH;
|
||||
const isForwarding = chatId === tabState.forwardMessages.toChatId;
|
||||
|
||||
return {
|
||||
availableReactions: global.reactions.availableReactions,
|
||||
@ -2252,7 +2374,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
isInScheduledList,
|
||||
botKeyboardMessageId,
|
||||
botKeyboardPlaceholder: keyboardMessage?.keyboardPlaceholder,
|
||||
isForwarding: chatId === tabState.forwardMessages.toChatId,
|
||||
isForwarding,
|
||||
forwardedMessagesCount: isForwarding ? forwardMessageIds!.length : undefined,
|
||||
pollModal: tabState.pollModal,
|
||||
stickersForEmoji: global.stickers.forEmoji.stickers,
|
||||
customEmojiForEmoji: global.customEmojis.forEmoji.stickers,
|
||||
@ -2307,7 +2430,10 @@ export default memo(withGlobal<OwnProps>(
|
||||
canPlayEffect,
|
||||
shouldPlayEffect,
|
||||
maxMessageLength,
|
||||
paidMessagesStars,
|
||||
shouldPaidMessageAutoApprove,
|
||||
isSilentPosting,
|
||||
isPaymentMessageConfirmDialogOpen: tabState.isPaymentMessageConfirmDialogOpen,
|
||||
};
|
||||
},
|
||||
)(Composer));
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
.checkBox {
|
||||
margin-top: 0.375rem;
|
||||
margin-inline: -1.125rem;
|
||||
padding-inline-start: 3.5rem;
|
||||
}
|
||||
73
src/components/common/PaymentMessageConfirmDialog.tsx
Normal file
73
src/components/common/PaymentMessageConfirmDialog.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import type { FC, StateHookSetter } from '../../lib/teact/teact';
|
||||
import React, { memo } from '../../lib/teact/teact';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import Checkbox from '../ui/Checkbox';
|
||||
import ConfirmDialog from '../ui/ConfirmDialog';
|
||||
|
||||
import styles from './PaymentMessageConfirmDialog.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
isOpen: boolean;
|
||||
onClose: NoneToVoidFunction;
|
||||
userName?: string;
|
||||
messagePriceInStars: number;
|
||||
messagesCount: number;
|
||||
shouldAutoApprove: boolean;
|
||||
setAutoApprove: StateHookSetter<boolean>;
|
||||
confirmHandler: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
const PaymentMessageConfirmDialog: FC<OwnProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
userName,
|
||||
messagePriceInStars,
|
||||
messagesCount,
|
||||
shouldAutoApprove: shouldPaidMessageAutoApprove,
|
||||
setAutoApprove: setShouldPaidMessageAutoApprove,
|
||||
confirmHandler,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
|
||||
const confirmPaymentMessage = messagesCount === 1 ? lang('ConfirmationModalPaymentForOneMessage', {
|
||||
user: userName,
|
||||
amount: messagePriceInStars,
|
||||
}, {
|
||||
withMarkdown: true,
|
||||
withNodes: true,
|
||||
}) : lang('ConfirmationModalPaymentForMessages', {
|
||||
user: userName,
|
||||
price: messagePriceInStars,
|
||||
amount: messagePriceInStars * messagesCount,
|
||||
count: messagesCount,
|
||||
}, {
|
||||
withMarkdown: true,
|
||||
withNodes: true,
|
||||
});
|
||||
|
||||
const confirmLabel = lang('ButtonPayForMessage', { count: messagesCount }, {
|
||||
withNodes: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
title={lang('TitleConfirmPayment')}
|
||||
confirmLabel={confirmLabel}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
confirmHandler={confirmHandler}
|
||||
>
|
||||
{confirmPaymentMessage}
|
||||
<Checkbox
|
||||
className={styles.checkBox}
|
||||
label={lang('DoNotAskAgain')}
|
||||
checked={shouldPaidMessageAutoApprove}
|
||||
onCheck={setShouldPaidMessageAutoApprove}
|
||||
/>
|
||||
</ConfirmDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PaymentMessageConfirmDialog);
|
||||
@ -30,6 +30,7 @@ export type OwnProps = {
|
||||
onSelectRecipient: (peerId: string, threadId?: ThreadId) => void;
|
||||
onClose: NoneToVoidFunction;
|
||||
onCloseAnimationEnd?: NoneToVoidFunction;
|
||||
isLowStackPriority?: boolean;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -54,6 +55,7 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
|
||||
onSelectRecipient,
|
||||
onClose,
|
||||
onCloseAnimationEnd,
|
||||
isLowStackPriority,
|
||||
}) => {
|
||||
const [search, setSearch] = useState('');
|
||||
const ids = useMemo(() => {
|
||||
@ -112,6 +114,7 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
|
||||
onSelectChatOrUser={onSelectRecipient}
|
||||
onClose={onClose}
|
||||
onCloseAnimationEnd={onCloseAnimationEnd}
|
||||
isLowStackPriority={isLowStackPriority}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
selectCurrentMessageList,
|
||||
selectIsChatWithSelf,
|
||||
selectIsCurrentUserPremium,
|
||||
selectPeerPaidMessagesStars,
|
||||
selectShouldSchedule,
|
||||
selectStickerSet,
|
||||
selectThreadInfo,
|
||||
@ -276,12 +277,13 @@ export default memo(withGlobal<OwnProps>(
|
||||
: stickerSetShortName ? { shortName: stickerSetShortName } : undefined;
|
||||
|
||||
const stickerSet = stickerSetInfo ? selectStickerSet(global, stickerSetInfo) : undefined;
|
||||
const paidMessagesStars = chatId ? selectPeerPaidMessagesStars(global, chatId) : undefined;
|
||||
|
||||
return {
|
||||
canScheduleUntilOnline: Boolean(chatId) && selectCanScheduleUntilOnline(global, chatId),
|
||||
canSendStickers,
|
||||
isSavedMessages,
|
||||
shouldSchedule: selectShouldSchedule(global),
|
||||
shouldSchedule: !paidMessagesStars && selectShouldSchedule(global),
|
||||
stickerSet,
|
||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||
shouldUpdateStickerSetOrder: global.settings.byKey.shouldUpdateStickerSetOrder,
|
||||
|
||||
@ -51,6 +51,7 @@ export type OwnProps = {
|
||||
onSelectChatOrUser: (chatOrUserId: string, threadId?: ThreadId) => void;
|
||||
onClose: NoneToVoidFunction;
|
||||
onCloseAnimationEnd?: NoneToVoidFunction;
|
||||
isLowStackPriority?: boolean;
|
||||
};
|
||||
|
||||
const CHAT_LIST_SLIDE = 0;
|
||||
@ -71,6 +72,7 @@ const ChatOrUserPicker: FC<OwnProps> = ({
|
||||
onSelectChatOrUser,
|
||||
onClose,
|
||||
onCloseAnimationEnd,
|
||||
isLowStackPriority,
|
||||
}) => {
|
||||
const { loadTopics } = getActions();
|
||||
|
||||
@ -323,6 +325,7 @@ const ChatOrUserPicker: FC<OwnProps> = ({
|
||||
className={buildClassName('ChatOrUserPicker', className)}
|
||||
onClose={onClose}
|
||||
onCloseAnimationEnd={onCloseAnimationEnd}
|
||||
isLowStackPriority={isLowStackPriority}
|
||||
>
|
||||
<Transition activeKey={activeKey} name="slideFade" slideClassName="ChatOrUserPicker_slide">
|
||||
{() => {
|
||||
|
||||
@ -339,6 +339,11 @@ function LeftColumn({
|
||||
case SettingsScreens.DoNotTranslate:
|
||||
setSettingsScreen(SettingsScreens.Language);
|
||||
return;
|
||||
|
||||
case SettingsScreens.PrivacyNoPaidMessages:
|
||||
setSettingsScreen(SettingsScreens.PrivacyMessages);
|
||||
return;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
.contacts_and_premium_option-title {
|
||||
.root {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
@ -17,7 +17,7 @@ function PrivacyLockedOption({ label }: OwnProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.contactsAndPremiumOptionTitle}
|
||||
className={styles.root}
|
||||
onClick={() => showNotification({ message: lang('OptionPremiumRequiredMessage') })}
|
||||
>
|
||||
<span>{label}</span>
|
||||
|
||||
@ -1,88 +1,245 @@
|
||||
import React, { memo, useMemo } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback, useMemo, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import { selectIsCurrentUserPremium, selectNewNoncontactPeersRequirePremium } from '../../../global/selectors';
|
||||
import { SettingsScreens } from '../../../types';
|
||||
|
||||
import {
|
||||
DEFAULT_CHARGE_FOR_MESSAGES,
|
||||
DEFAULT_MAXIMUM_CHARGE_FOR_MESSAGES,
|
||||
MINIMUM_CHARGE_FOR_MESSAGES,
|
||||
} from '../../../config';
|
||||
import {
|
||||
selectIsCurrentUserPremium,
|
||||
selectNewNoncontactPeersRequirePremium,
|
||||
selectNonContactPeersPaidStars,
|
||||
} from '../../../global/selectors';
|
||||
import { formatCurrencyAsString } from '../../../util/formatCurrency';
|
||||
import { formatStarsAsText } from '../../../util/localization/format';
|
||||
|
||||
import useDebouncedCallback from '../../../hooks/useDebouncedCallback';
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import RadioGroup from '../../ui/RadioGroup';
|
||||
import RangeSlider from '../../ui/RangeSlider';
|
||||
import PremiumStatusItem from './PremiumStatusItem';
|
||||
import PrivacyLockedOption from './PrivacyLockedOption';
|
||||
|
||||
type OwnProps = {
|
||||
isActive?: boolean;
|
||||
onReset: VoidFunction;
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
shouldNewNonContactPeersRequirePremium?: boolean;
|
||||
shouldChargeForMessages?: boolean;
|
||||
canLimitNewMessagesWithoutPremium?: boolean;
|
||||
canChargeForMessages?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
starsUsdWithdrawRate: number;
|
||||
starsPaidMessageCommissionPermille: number;
|
||||
starsPaidMessageAmountMax?: number;
|
||||
nonContactPeersPaidStars: number;
|
||||
noPaidReactionsForUsersCount: number;
|
||||
};
|
||||
|
||||
function PrivacyMessages({
|
||||
isActive,
|
||||
canLimitNewMessagesWithoutPremium,
|
||||
canChargeForMessages,
|
||||
shouldNewNonContactPeersRequirePremium,
|
||||
shouldChargeForMessages,
|
||||
nonContactPeersPaidStars,
|
||||
isCurrentUserPremium,
|
||||
starsPaidMessageCommissionPermille,
|
||||
starsPaidMessageAmountMax,
|
||||
starsUsdWithdrawRate,
|
||||
noPaidReactionsForUsersCount,
|
||||
onReset,
|
||||
onScreenSelect,
|
||||
}: OwnProps & StateProps) {
|
||||
const { updateGlobalPrivacySettings } = getActions();
|
||||
const lang = useOldLang();
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
const canChange = isCurrentUserPremium || canLimitNewMessagesWithoutPremium;
|
||||
const canChangeForContactsAndPremium = isCurrentUserPremium || canLimitNewMessagesWithoutPremium;
|
||||
const canChangeChargeForMessages = isCurrentUserPremium && canChargeForMessages;
|
||||
const [chargeForMessages, setChargeForMessages] = useState<number>(nonContactPeersPaidStars);
|
||||
|
||||
const options = useMemo(() => {
|
||||
return [
|
||||
{ value: 'everybody', label: lang('P2PEverybody') },
|
||||
{ value: 'everybody', label: oldLang('P2PEverybody') },
|
||||
{
|
||||
value: 'contacts_and_premium',
|
||||
label: canChange ? (
|
||||
lang('PrivacyMessagesContactsAndPremium')
|
||||
label: canChangeForContactsAndPremium ? (
|
||||
oldLang('PrivacyMessagesContactsAndPremium')
|
||||
) : (
|
||||
<PrivacyLockedOption label={lang('PrivacyMessagesContactsAndPremium')} />
|
||||
<PrivacyLockedOption label={oldLang('PrivacyMessagesContactsAndPremium')} />
|
||||
),
|
||||
hidden: !canChange,
|
||||
hidden: !canChangeForContactsAndPremium,
|
||||
},
|
||||
{
|
||||
value: 'charge_for_messages',
|
||||
label: canChangeChargeForMessages ? (
|
||||
lang('PrivacyChargeForMessages')
|
||||
) : (
|
||||
<PrivacyLockedOption label={lang('PrivacyChargeForMessages')} />
|
||||
),
|
||||
hidden: !canChangeChargeForMessages,
|
||||
},
|
||||
];
|
||||
}, [lang, canChange]);
|
||||
}, [oldLang, lang, canChangeForContactsAndPremium, canChangeChargeForMessages]);
|
||||
|
||||
const handleChange = useLastCallback((privacy: string) => {
|
||||
updateGlobalPrivacySettings({ shouldNewNonContactPeersRequirePremium: privacy === 'contacts_and_premium' });
|
||||
updateGlobalPrivacySettings({
|
||||
shouldNewNonContactPeersRequirePremium: privacy === 'contacts_and_premium',
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
nonContactPeersPaidStars: privacy === 'charge_for_messages' ? chargeForMessages : null,
|
||||
});
|
||||
});
|
||||
|
||||
const updateGlobalPrivacySettingsWithDebounced = useDebouncedCallback((value: number) => {
|
||||
updateGlobalPrivacySettings({
|
||||
nonContactPeersPaidStars: value,
|
||||
});
|
||||
}, [updateGlobalPrivacySettings], 300, true);
|
||||
|
||||
const handleChargeForMessagesChange = useCallback((value: number) => {
|
||||
setChargeForMessages(value);
|
||||
updateGlobalPrivacySettingsWithDebounced(value);
|
||||
}, [setChargeForMessages, updateGlobalPrivacySettingsWithDebounced]);
|
||||
|
||||
const renderValueForStarsRange = useCallback((value: number) => {
|
||||
return formatStarsAsText(lang, value);
|
||||
}, [lang]);
|
||||
|
||||
function renderSectionStarsAmountForPaidMessages() {
|
||||
return (
|
||||
<div className="settings-item">
|
||||
<h4 className="settings-item-header" dir={oldLang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('SectionTitleStarsForForMessages')}
|
||||
</h4>
|
||||
<RangeSlider
|
||||
isCenteredLayout
|
||||
min={MINIMUM_CHARGE_FOR_MESSAGES}
|
||||
max={starsPaidMessageAmountMax}
|
||||
value={chargeForMessages}
|
||||
onChange={handleChargeForMessagesChange}
|
||||
renderValue={renderValueForStarsRange}
|
||||
/>
|
||||
<p className="settings-item-description-larger" dir={oldLang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('SectionDescriptionStarsForForMessages', {
|
||||
percent: starsPaidMessageCommissionPermille * 100,
|
||||
amount: formatCurrencyAsString(
|
||||
chargeForMessages * starsUsdWithdrawRate * starsPaidMessageCommissionPermille,
|
||||
'USD',
|
||||
lang.code,
|
||||
|
||||
),
|
||||
}, {
|
||||
withNodes: true,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSectionNoPaidMessagesForUsers() {
|
||||
const itemSubtitle = !noPaidReactionsForUsersCount ? lang('SubtitlePrivacyAddUsers')
|
||||
: oldLang('Users', noPaidReactionsForUsersCount, 'i');
|
||||
|
||||
return (
|
||||
<div className="settings-item">
|
||||
<h4 className="settings-item-header" dir={oldLang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('RemoveFeeTitle')}
|
||||
</h4>
|
||||
<ListItem
|
||||
narrow
|
||||
icon="delete-user"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => {
|
||||
onScreenSelect(SettingsScreens.PrivacyNoPaidMessages);
|
||||
}}
|
||||
>
|
||||
<div className="multiline-item full-size">
|
||||
<span className="title">{lang('ExceptionTitlePrivacyChargeForMessages')}</span>
|
||||
<span className="subtitle">{
|
||||
itemSubtitle
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</ListItem>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
const selectedValue = useMemo(() => {
|
||||
if (shouldChargeForMessages) return 'charge_for_messages';
|
||||
if (shouldNewNonContactPeersRequirePremium) return 'contacts_and_premium';
|
||||
return 'everybody';
|
||||
}, [shouldChargeForMessages, shouldNewNonContactPeersRequirePremium]);
|
||||
|
||||
const privacyDescription = useMemo(() => {
|
||||
if (shouldChargeForMessages) return lang('PrivacyDescriptionChargeForMessages');
|
||||
return lang('PrivacyDescriptionMessagesContactsAndPremium');
|
||||
}, [shouldChargeForMessages, lang]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="settings-item">
|
||||
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('PrivacyMessagesTitle')}
|
||||
<h4 className="settings-item-header" dir={oldLang.isRtl ? 'rtl' : undefined}>
|
||||
{oldLang('PrivacyMessagesTitle')}
|
||||
</h4>
|
||||
<RadioGroup
|
||||
name="privacy-messages"
|
||||
options={options}
|
||||
onChange={handleChange}
|
||||
selected={shouldNewNonContactPeersRequirePremium ? 'contacts_and_premium' : 'everybody'}
|
||||
selected={selectedValue}
|
||||
/>
|
||||
<p className="settings-item-description-larger" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('Privacy.Messages.SectionFooter')}
|
||||
<p className="settings-item-description-larger" dir={oldLang.isRtl ? 'rtl' : undefined}>
|
||||
{privacyDescription}
|
||||
</p>
|
||||
</div>
|
||||
{!canChange && <PremiumStatusItem premiumSection="message_privacy" />}
|
||||
{canChangeChargeForMessages
|
||||
&& selectedValue === 'charge_for_messages' && renderSectionStarsAmountForPaidMessages()}
|
||||
{canChangeChargeForMessages && selectedValue === 'charge_for_messages' && renderSectionNoPaidMessagesForUsers()}
|
||||
{!isCurrentUserPremium && <PremiumStatusItem premiumSection="message_privacy" />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(withGlobal<OwnProps>((global): StateProps => {
|
||||
const nonContactPeersPaidStars = selectNonContactPeersPaidStars(global);
|
||||
|
||||
const starsUsdWithdrawRateX1000 = global.appConfig?.starsUsdWithdrawRateX1000;
|
||||
const starsUsdWithdrawRate = starsUsdWithdrawRateX1000 ? starsUsdWithdrawRateX1000 / 1000 : 1;
|
||||
const configStarsPaidMessageCommissionPermille = global.appConfig?.starsPaidMessageCommissionPermille;
|
||||
const starsPaidMessageCommissionPermille = configStarsPaidMessageCommissionPermille
|
||||
? configStarsPaidMessageCommissionPermille / 1000 : 100;
|
||||
|
||||
const noPaidReactionsForUsersCount = global.settings.privacy.noPaidMessages?.allowUserIds.length || 0;
|
||||
|
||||
return {
|
||||
shouldNewNonContactPeersRequirePremium: selectNewNoncontactPeersRequirePremium(global),
|
||||
shouldChargeForMessages: Boolean(nonContactPeersPaidStars),
|
||||
nonContactPeersPaidStars: nonContactPeersPaidStars || DEFAULT_CHARGE_FOR_MESSAGES,
|
||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||
canLimitNewMessagesWithoutPremium: global.appConfig?.canLimitNewMessagesWithoutPremium,
|
||||
canChargeForMessages: global.appConfig?.starsPaidMessagesAvailable,
|
||||
starsPaidMessageAmountMax: global.appConfig?.starsPaidMessageAmountMax || DEFAULT_MAXIMUM_CHARGE_FOR_MESSAGES,
|
||||
starsPaidMessageCommissionPermille,
|
||||
starsUsdWithdrawRate,
|
||||
noPaidReactionsForUsersCount,
|
||||
};
|
||||
})(PrivacyMessages));
|
||||
|
||||
@ -141,6 +141,10 @@ const PRIVACY_GROUP_CHATS_SCREENS = [
|
||||
SettingsScreens.PrivacyGroupChatsDeniedContacts,
|
||||
];
|
||||
|
||||
const PRIVACY_MESSAGES_SCREENS = [
|
||||
SettingsScreens.PrivacyNoPaidMessages,
|
||||
];
|
||||
|
||||
export type OwnProps = {
|
||||
isActive: boolean;
|
||||
currentScreen: SettingsScreens;
|
||||
@ -224,6 +228,7 @@ const Settings: FC<OwnProps> = ({
|
||||
[SettingsScreens.PrivacyForwarding]: PRIVACY_FORWARDING_SCREENS.includes(activeScreen),
|
||||
[SettingsScreens.PrivacyVoiceMessages]: PRIVACY_VOICE_MESSAGES_SCREENS.includes(activeScreen),
|
||||
[SettingsScreens.PrivacyGroupChats]: PRIVACY_GROUP_CHATS_SCREENS.includes(activeScreen),
|
||||
[SettingsScreens.PrivacyMessages]: PRIVACY_MESSAGES_SCREENS.includes(activeScreen),
|
||||
};
|
||||
|
||||
const isTwoFaScreen = TWO_FA_SCREENS.includes(activeScreen);
|
||||
@ -370,13 +375,14 @@ const Settings: FC<OwnProps> = ({
|
||||
case SettingsScreens.PrivacyForwardingAllowedContacts:
|
||||
case SettingsScreens.PrivacyVoiceMessagesAllowedContacts:
|
||||
case SettingsScreens.PrivacyGroupChatsAllowedContacts:
|
||||
case SettingsScreens.PrivacyNoPaidMessages:
|
||||
return (
|
||||
<SettingsPrivacyVisibilityExceptionList
|
||||
isAllowList
|
||||
usersOnly={currentScreen === SettingsScreens.PrivacyNoPaidMessages}
|
||||
withPremiumCategory={currentScreen === SettingsScreens.PrivacyGroupChatsAllowedContacts}
|
||||
withMiniAppsCategory={currentScreen === SettingsScreens.PrivacyGiftsAllowedContacts}
|
||||
screen={currentScreen}
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isScreenActive || privacyAllowScreens[currentScreen]}
|
||||
onReset={handleReset}
|
||||
/>
|
||||
@ -396,7 +402,6 @@ const Settings: FC<OwnProps> = ({
|
||||
return (
|
||||
<SettingsPrivacyVisibilityExceptionList
|
||||
screen={currentScreen}
|
||||
onScreenSelect={onScreenSelect}
|
||||
isActive={isScreenActive}
|
||||
onReset={handleReset}
|
||||
/>
|
||||
@ -407,6 +412,7 @@ const Settings: FC<OwnProps> = ({
|
||||
<PrivacyMessages
|
||||
isActive={isScreenActive}
|
||||
onReset={handleReset}
|
||||
onScreenSelect={onScreenSelect}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@ -163,6 +163,9 @@ const SettingsHeader: FC<OwnProps> = ({
|
||||
case SettingsScreens.PrivacyPhoneP2PDeniedContacts:
|
||||
return <h3>{oldLang('NeverAllow')}</h3>;
|
||||
|
||||
case SettingsScreens.PrivacyNoPaidMessages:
|
||||
return <h3>{lang('RemoveFeeTitle')}</h3>;
|
||||
|
||||
case SettingsScreens.Performance:
|
||||
return <h3>{lang('MenuAnimations')}</h3>;
|
||||
|
||||
|
||||
@ -34,6 +34,7 @@ type StateProps = {
|
||||
canDisplayAutoarchiveSetting: boolean;
|
||||
shouldArchiveAndMuteNewNonContact?: boolean;
|
||||
shouldNewNonContactPeersRequirePremium?: boolean;
|
||||
shouldChargeForMessages: boolean;
|
||||
canDisplayChatInTitle?: boolean;
|
||||
privacy: GlobalState['settings']['privacy'];
|
||||
};
|
||||
@ -50,6 +51,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
canDisplayAutoarchiveSetting,
|
||||
shouldArchiveAndMuteNewNonContact,
|
||||
shouldNewNonContactPeersRequirePremium,
|
||||
shouldChargeForMessages,
|
||||
canDisplayChatInTitle,
|
||||
canSetPasscode,
|
||||
privacy,
|
||||
@ -332,9 +334,10 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
|
||||
<div className="multiline-item">
|
||||
<span className="title">{oldLang('PrivacyMessagesTitle')}</span>
|
||||
<span className="subtitle" dir="auto">
|
||||
{shouldNewNonContactPeersRequirePremium
|
||||
? oldLang('PrivacyMessagesContactsAndPremium')
|
||||
: oldLang('P2PEverybody')}
|
||||
{shouldChargeForMessages ? lang('PrivacyPaidMessagesValue')
|
||||
: shouldNewNonContactPeersRequirePremium
|
||||
? oldLang('PrivacyMessagesContactsAndPremium')
|
||||
: oldLang('P2PEverybody')}
|
||||
</span>
|
||||
</div>
|
||||
</ListItem>
|
||||
@ -402,7 +405,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
settings: {
|
||||
byKey: {
|
||||
hasPassword, isSensitiveEnabled, canChangeSensitive, shouldArchiveAndMuteNewNonContact,
|
||||
canDisplayChatInTitle, shouldNewNonContactPeersRequirePremium,
|
||||
canDisplayChatInTitle, shouldNewNonContactPeersRequirePremium, nonContactPeersPaidStars,
|
||||
},
|
||||
privacy,
|
||||
},
|
||||
@ -413,6 +416,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
appConfig,
|
||||
} = global;
|
||||
|
||||
const shouldChargeForMessages = Boolean(nonContactPeersPaidStars);
|
||||
|
||||
return {
|
||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||
hasPassword,
|
||||
@ -424,6 +429,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
shouldArchiveAndMuteNewNonContact,
|
||||
canChangeSensitive,
|
||||
shouldNewNonContactPeersRequirePremium,
|
||||
shouldChargeForMessages,
|
||||
privacy,
|
||||
canDisplayChatInTitle,
|
||||
canSetPasscode: selectCanSetPasscode(global),
|
||||
|
||||
@ -31,9 +31,9 @@ export type OwnProps = {
|
||||
isAllowList?: boolean;
|
||||
withPremiumCategory?: boolean;
|
||||
withMiniAppsCategory?: boolean;
|
||||
usersOnly?: boolean;
|
||||
screen: SettingsScreens;
|
||||
isActive?: boolean;
|
||||
onScreenSelect: (screen: SettingsScreens) => void;
|
||||
onReset: () => void;
|
||||
};
|
||||
|
||||
@ -52,7 +52,7 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
|
||||
isActive,
|
||||
currentUserId,
|
||||
settings,
|
||||
onScreenSelect,
|
||||
usersOnly = false,
|
||||
onReset,
|
||||
}) => {
|
||||
const { setPrivacySettings } = getActions();
|
||||
@ -120,7 +120,10 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
|
||||
const user = usersById[chatId];
|
||||
const isDeleted = user && isDeletedUser(user);
|
||||
const isChannel = chat && isChatChannel(chat);
|
||||
return chatId !== currentUserId && chatId !== SERVICE_NOTIFICATIONS_USER_ID && !isChannel && !isDeleted;
|
||||
return (!usersOnly || user)
|
||||
&& chatId !== currentUserId
|
||||
&& chatId !== SERVICE_NOTIFICATIONS_USER_ID
|
||||
&& !isChannel && !isDeleted;
|
||||
});
|
||||
|
||||
const filteredChats = filterPeersByQuery({ ids: chatIds, query: searchQuery });
|
||||
@ -132,7 +135,7 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
|
||||
...selectedContactIds,
|
||||
...chatIds,
|
||||
]);
|
||||
}, [folderAllOrderedIds, folderArchivedOrderedIds, selectedContactIds, searchQuery, currentUserId]);
|
||||
}, [folderAllOrderedIds, folderArchivedOrderedIds, selectedContactIds, searchQuery, currentUserId, usersOnly]);
|
||||
|
||||
const handleSelectedCategoriesChange = useCallback((value: CustomPeerType[]) => {
|
||||
setNewSelectedCategoryTypes(value);
|
||||
@ -154,13 +157,13 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
|
||||
: (newSelectedCategoryTypes.includes(customPeerBots.type) ? 'allow' : 'disallow'),
|
||||
});
|
||||
|
||||
onScreenSelect(SettingsScreens.Privacy);
|
||||
onReset();
|
||||
}, [
|
||||
isAllowList,
|
||||
withMiniAppsCategory,
|
||||
newSelectedCategoryTypes,
|
||||
newSelectedContactIds,
|
||||
onScreenSelect,
|
||||
onReset,
|
||||
screen,
|
||||
customPeerBots,
|
||||
]);
|
||||
@ -244,6 +247,8 @@ function getCurrentPrivacySettings(global: GlobalState, screen: SettingsScreens)
|
||||
case SettingsScreens.PrivacyGroupChatsDeniedContacts:
|
||||
case SettingsScreens.PrivacyGroupChatsAllowedContacts:
|
||||
return privacy.chatInvite;
|
||||
case SettingsScreens.PrivacyNoPaidMessages:
|
||||
return privacy.noPaidMessages;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@ -49,6 +49,8 @@ export function getPrivacyKey(screen: SettingsScreens): ApiPrivacyKey | undefine
|
||||
return 'phoneP2P';
|
||||
case SettingsScreens.PrivacyAddByPhone:
|
||||
return 'addByPhone';
|
||||
case SettingsScreens.PrivacyNoPaidMessages:
|
||||
return 'noPaidMessages';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
@ -9,7 +9,6 @@ import type { MessageList } from '../../types';
|
||||
|
||||
import { selectCurrentMessageList, selectTabState } from '../../global/selectors';
|
||||
import getReadableErrorText from '../../util/getReadableErrorText';
|
||||
import { pick } from '../../util/iteratees';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
@ -49,7 +48,7 @@ const Dialogs: FC<StateProps> = ({ dialogs, currentMessageList }) => {
|
||||
}
|
||||
|
||||
sendMessage({
|
||||
contact: pick(contactRequest, ['firstName', 'lastName', 'phoneNumber']),
|
||||
contact: contactRequest,
|
||||
messageList: currentMessageList,
|
||||
});
|
||||
closeModal();
|
||||
|
||||
@ -21,7 +21,7 @@ const Notifications: FC<StateProps> = ({ notifications }) => {
|
||||
return (
|
||||
<div id="Notifications">
|
||||
{notifications.map((notification) => (
|
||||
<Notification notification={notification} />
|
||||
<Notification key={notification.localId} notification={notification} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -80,7 +80,7 @@ import ContactGreeting from './ContactGreeting';
|
||||
import MessageListAccountInfo from './MessageListAccountInfo';
|
||||
import MessageListContent from './MessageListContent';
|
||||
import NoMessages from './NoMessages';
|
||||
import PremiumRequiredMessage from './PremiumRequiredMessage';
|
||||
import RequirementToContactMessage from './RequirementToContactMessage';
|
||||
|
||||
import './MessageList.scss';
|
||||
|
||||
@ -97,6 +97,7 @@ type OwnProps = {
|
||||
withDefaultBg: boolean;
|
||||
onIntersectPinnedMessage: OnIntersectPinnedMessage;
|
||||
isContactRequirePremium?: boolean;
|
||||
paidMessagesStars?: number;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -187,6 +188,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
isServiceNotificationsChat,
|
||||
currentUserId,
|
||||
isContactRequirePremium,
|
||||
paidMessagesStars,
|
||||
areAdsEnabled,
|
||||
channelJoinInfo,
|
||||
isChatProtected,
|
||||
@ -689,8 +691,10 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
{restrictionReason ? restrictionReason.text : `This is a private ${isChannelChat ? 'channel' : 'chat'}`}
|
||||
</span>
|
||||
</div>
|
||||
) : paidMessagesStars && isPrivate && !hasMessages && !shouldRenderGreeting ? (
|
||||
<RequirementToContactMessage paidMessagesStars={paidMessagesStars} userId={chatId} />
|
||||
) : isContactRequirePremium && !hasMessages ? (
|
||||
<PremiumRequiredMessage userId={chatId} />
|
||||
<RequirementToContactMessage userId={chatId} />
|
||||
) : (isBot || isNonContact) && !hasMessages ? (
|
||||
<MessageListAccountInfo chatId={chatId} />
|
||||
) : shouldRenderGreeting ? (
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import type { RefObject } from 'react';
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { getIsHeavyAnimating, memo } from '../../lib/teact/teact';
|
||||
import { getActions } from '../../global';
|
||||
import { getActions, getGlobal } from '../../global';
|
||||
|
||||
import type { MessageListType, ThreadId } from '../../types';
|
||||
import type { ApiMessage } from '../../api/types';
|
||||
import type { IAlbum, MessageListType, ThreadId } from '../../types';
|
||||
import type { Signal } from '../../util/signals';
|
||||
import type { MessageDateGroup } from './helpers/groupMessages';
|
||||
import type { OnIntersectPinnedMessage } from './hooks/usePinnedMessage';
|
||||
@ -13,10 +14,12 @@ import { SCHEDULED_WHEN_ONLINE } from '../../config';
|
||||
import {
|
||||
getMessageHtmlId,
|
||||
getMessageOriginalId,
|
||||
getPeerTitle,
|
||||
isActionMessage,
|
||||
isOwnMessage,
|
||||
isServiceNotificationMessage,
|
||||
} from '../../global/helpers';
|
||||
import { selectSender } from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { formatHumanDate } from '../../util/dates/dateFormat';
|
||||
import { compact } from '../../util/iteratees';
|
||||
@ -24,6 +27,7 @@ import { isAlbum } from './helpers/groupMessages';
|
||||
import { preventMessageInputBlur } from './helpers/preventMessageInputBlur';
|
||||
|
||||
import useDerivedSignal from '../../hooks/useDerivedSignal';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
|
||||
import useMessageObservers from './hooks/useMessageObservers';
|
||||
@ -131,12 +135,42 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
);
|
||||
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
const unreadDivider = (
|
||||
<div className={buildClassName(UNREAD_DIVIDER_CLASS, 'local-action-message')} key="unread-messages">
|
||||
<span>{oldLang('UnreadMessages')}</span>
|
||||
</div>
|
||||
);
|
||||
const renderPaidMessageAction = (message: ApiMessage, album?: IAlbum) => {
|
||||
if (message.paidMessageStars) {
|
||||
const messagesLength = album?.messages?.length || 1;
|
||||
const amount = message.paidMessageStars * messagesLength;
|
||||
return (
|
||||
<div
|
||||
className={buildClassName('local-action-message')}
|
||||
key={`paid-messages-action-${message.id}`}
|
||||
>
|
||||
<span>{
|
||||
message.isOutgoing
|
||||
? lang('ActionPaidOneMessageOutgoing', {
|
||||
amount,
|
||||
})
|
||||
: (() => {
|
||||
const sender = selectSender(getGlobal(), message);
|
||||
const userTitle = sender ? getPeerTitle(lang, sender) : '';
|
||||
return lang('ActionPaidOneMessageIncoming', {
|
||||
user: userTitle,
|
||||
amount,
|
||||
});
|
||||
})()
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const messageCountToAnimate = noAppearanceAnimation ? 0 : messageGroups.reduce((acc, messageGroup) => {
|
||||
return acc + messageGroup.senderGroups.flat().length;
|
||||
}, 0);
|
||||
@ -228,6 +262,7 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
|
||||
return compact([
|
||||
message.id === memoUnreadDividerBeforeIdRef.current && unreadDivider,
|
||||
message.paidMessageStars && !withUsers && renderPaidMessageAction(message, album),
|
||||
<Message
|
||||
key={key}
|
||||
message={message}
|
||||
|
||||
@ -65,7 +65,9 @@
|
||||
> .Button {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
/* stylelint-disable plugin/no-low-performance-animation-properties */
|
||||
transition:
|
||||
border-radius 0.15s,
|
||||
opacity var(--select-transition),
|
||||
transform var(--select-transition),
|
||||
background-color 0.15s,
|
||||
@ -162,7 +164,6 @@
|
||||
z-index: var(--z-middle-footer);
|
||||
|
||||
transform: translate3d(0, 0, 0);
|
||||
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||
transition: top 200ms, transform var(--layer-transition);
|
||||
|
||||
body.no-page-transitions & {
|
||||
@ -170,7 +171,6 @@
|
||||
}
|
||||
|
||||
body.no-right-column-animations & {
|
||||
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||
transition: top 200ms !important;
|
||||
}
|
||||
|
||||
|
||||
@ -50,6 +50,7 @@ import {
|
||||
selectIsInSelectMode,
|
||||
selectIsRightColumnShown,
|
||||
selectIsUserBlocked,
|
||||
selectPeerPaidMessagesStars,
|
||||
selectPinnedIds,
|
||||
selectTabState,
|
||||
selectTheme,
|
||||
@ -153,6 +154,7 @@ type StateProps = {
|
||||
canShowOpenChatButton?: boolean;
|
||||
isContactRequirePremium?: boolean;
|
||||
topics?: Record<number, ApiTopic>;
|
||||
paidMessagesStars?: number;
|
||||
};
|
||||
|
||||
function isImage(item: DataTransferItem) {
|
||||
@ -213,6 +215,7 @@ function MiddleColumn({
|
||||
canShowOpenChatButton,
|
||||
isContactRequirePremium,
|
||||
topics,
|
||||
paidMessagesStars,
|
||||
}: OwnProps & StateProps) {
|
||||
const {
|
||||
openChat,
|
||||
@ -551,6 +554,7 @@ function MiddleColumn({
|
||||
onNotchToggle={setIsNotchShown}
|
||||
isReady={isReady}
|
||||
isContactRequirePremium={isContactRequirePremium}
|
||||
paidMessagesStars={paidMessagesStars}
|
||||
withBottomShift={withMessageListBottomShift}
|
||||
withDefaultBg={Boolean(!customBackground && !backgroundColor)}
|
||||
onIntersectPinnedMessage={renderingHandleIntersectPinnedMessage!}
|
||||
@ -800,7 +804,10 @@ export default memo(withGlobal<OwnProps>(
|
||||
)
|
||||
);
|
||||
|
||||
const isContactRequirePremium = selectUserFullInfo(global, chatId)?.isContactRequirePremium;
|
||||
const userFull = selectUserFullInfo(global, chatId);
|
||||
|
||||
const isContactRequirePremium = userFull?.isContactRequirePremium;
|
||||
const paidMessagesStars = selectPeerPaidMessagesStars(global, chatId);
|
||||
|
||||
return {
|
||||
...state,
|
||||
@ -838,6 +845,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
canShowOpenChatButton,
|
||||
isContactRequirePremium,
|
||||
topics,
|
||||
paidMessagesStars,
|
||||
};
|
||||
},
|
||||
)(MiddleColumn));
|
||||
|
||||
@ -26,6 +26,7 @@ import BotAdPane from './panes/BotAdPane';
|
||||
import BotVerificationPane from './panes/BotVerificationPane';
|
||||
import ChatReportPane from './panes/ChatReportPane';
|
||||
import HeaderPinnedMessage from './panes/HeaderPinnedMessage';
|
||||
import PaidMessageChargePane from './panes/PaidMessageChargePane';
|
||||
|
||||
import styles from './MiddleHeaderPanes.module.scss';
|
||||
|
||||
@ -70,6 +71,7 @@ const MiddleHeaderPanes = ({
|
||||
const [getChatReportState, setChatReportState] = useSignal<PaneState>(FALLBACK_PANE_STATE);
|
||||
const [getBotAdState, setBotAdState] = useSignal<PaneState>(FALLBACK_PANE_STATE);
|
||||
const [getBotVerificationState, setBotVerificationState] = useSignal<PaneState>(FALLBACK_PANE_STATE);
|
||||
const [getPaidMessageChargeState, setPaidMessageChargeState] = useSignal<PaneState>(FALLBACK_PANE_STATE);
|
||||
|
||||
const isPinnedMessagesFullWidth = isAudioPlayerRendered || !isDesktop;
|
||||
|
||||
@ -94,10 +96,11 @@ const MiddleHeaderPanes = ({
|
||||
const groupCallState = getGroupCallState();
|
||||
const chatReportState = getChatReportState();
|
||||
const botAdState = getBotAdState();
|
||||
const paidMessageState = getPaidMessageChargeState();
|
||||
|
||||
// Keep in sync with the order of the panes in the DOM
|
||||
const stateArray = [audioPlayerState, groupCallState,
|
||||
chatReportState, botVerificationState, pinnedState, botAdState];
|
||||
chatReportState, botVerificationState, pinnedState, botAdState, paidMessageState];
|
||||
|
||||
const isFirstRender = isFirstRenderRef.current;
|
||||
const totalHeight = stateArray.reduce((acc, state) => acc + state.height, 0);
|
||||
@ -111,7 +114,7 @@ const MiddleHeaderPanes = ({
|
||||
'--middle-header-panes-height': `${totalHeight}px`,
|
||||
});
|
||||
}, [getAudioPlayerState, getGroupCallState, getPinnedState,
|
||||
getChatReportState, getBotAdState, getBotVerificationState]);
|
||||
getChatReportState, getBotAdState, getBotVerificationState, getPaidMessageChargeState]);
|
||||
|
||||
if (!shouldRender) return undefined;
|
||||
|
||||
@ -140,6 +143,10 @@ const MiddleHeaderPanes = ({
|
||||
peerId={chatId}
|
||||
onPaneStateChange={setBotVerificationState}
|
||||
/>
|
||||
<PaidMessageChargePane
|
||||
peerId={chatId}
|
||||
onPaneStateChange={setPaidMessageChargeState}
|
||||
/>
|
||||
<HeaderPinnedMessage
|
||||
chatId={chatId}
|
||||
threadId={threadId}
|
||||
|
||||
@ -8,8 +8,8 @@
|
||||
|
||||
.button {
|
||||
background: var(--pattern-color);
|
||||
width: 10rem;
|
||||
margin-top: 0.5rem;
|
||||
width: auto;
|
||||
margin-top: 1.0625rem;
|
||||
text-transform: none;
|
||||
color: var(--color-white);
|
||||
height: 2.25rem;
|
||||
@ -28,8 +28,8 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: var(--pattern-color);
|
||||
max-width: 15rem;
|
||||
padding: 0.75rem 0;
|
||||
max-width: 13.5rem;
|
||||
padding: 1.0625rem 0;
|
||||
border-radius: 1.5rem;
|
||||
|
||||
&[dir="rtl"] {
|
||||
@ -60,7 +60,23 @@
|
||||
}
|
||||
|
||||
.description {
|
||||
white-space: pre;
|
||||
text-align: center;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
padding: 0 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.starIconContainer {
|
||||
font-weight: var(--font-weight-medium);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.starIcon {
|
||||
margin-inline-start: 0 !important;
|
||||
margin-inline-end: 0.0625rem !important;
|
||||
}
|
||||
@ -3,20 +3,25 @@ import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import { getUserFirstOrLastName } from '../../global/helpers';
|
||||
import { selectTheme, selectUser } from '../../global/selectors';
|
||||
import { formatStarsAsIcon } from '../../util/localization/format';
|
||||
import { LOCAL_TGS_URLS } from '../common/helpers/animatedAssets';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import AnimatedIconWithPreview from '../common/AnimatedIconWithPreview';
|
||||
import Icon from '../common/icons/Icon';
|
||||
import Sparkles from '../common/Sparkles';
|
||||
import Button from '../ui/Button';
|
||||
|
||||
import styles from './PremiumRequiredMessage.module.scss';
|
||||
import styles from './RequirementToContactMessage.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
userId: string;
|
||||
paidMessagesStars?: number;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -24,12 +29,15 @@ type StateProps = {
|
||||
userName?: string;
|
||||
};
|
||||
|
||||
function PremiumRequiredMessage({ patternColor, userName }: StateProps) {
|
||||
const lang = useOldLang();
|
||||
const { openPremiumModal } = getActions();
|
||||
function RequirementToContactMessage({ patternColor, userName, paidMessagesStars }: OwnProps & StateProps) {
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
const { openPremiumModal, openStarsBalanceModal } = getActions();
|
||||
|
||||
const handleOpenPremiumModal = useLastCallback(() => openPremiumModal());
|
||||
|
||||
const handleGetMoreStars = useLastCallback(() => { openStarsBalanceModal({}); });
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.inner}>
|
||||
@ -43,15 +51,41 @@ function PremiumRequiredMessage({ patternColor, userName }: StateProps) {
|
||||
<Icon name="comments-sticker" className={styles.commentsIcon} />
|
||||
</div>
|
||||
<span className={styles.description}>
|
||||
{renderText(lang('MessageLockedPremium', userName), ['simple_markdown'])}
|
||||
{
|
||||
paidMessagesStars
|
||||
? lang('FirstMessageInPaidMessagesChat', {
|
||||
user: userName,
|
||||
amount: formatStarsAsIcon(lang,
|
||||
paidMessagesStars,
|
||||
{
|
||||
asFont: true,
|
||||
className: styles.starIcon,
|
||||
containerClassName: styles.starIconContainer,
|
||||
}),
|
||||
}, {
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
})
|
||||
: renderText(oldLang('MessageLockedPremium', userName), ['simple_markdown'])
|
||||
}
|
||||
</span>
|
||||
<Button
|
||||
color="translucent-black"
|
||||
size="tiny"
|
||||
onClick={handleOpenPremiumModal}
|
||||
size="default"
|
||||
pill
|
||||
onClick={paidMessagesStars ? handleGetMoreStars : handleOpenPremiumModal}
|
||||
className={styles.button}
|
||||
>
|
||||
{lang('MessagePremiumUnlock')}
|
||||
{
|
||||
paidMessagesStars
|
||||
? (
|
||||
<>
|
||||
{lang('ButtonBuyStars')}
|
||||
<Sparkles preset="button" />
|
||||
</>
|
||||
)
|
||||
: oldLang('MessagePremiumUnlock')
|
||||
}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -68,5 +102,5 @@ export default memo(
|
||||
patternColor,
|
||||
userName: getUserFirstOrLastName(user),
|
||||
};
|
||||
})(PremiumRequiredMessage),
|
||||
})(RequirementToContactMessage),
|
||||
);
|
||||
@ -86,6 +86,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.sendButtonStar {
|
||||
margin-inline-start: 0 !important;
|
||||
margin-inline-end: 0.125rem !important;
|
||||
}
|
||||
|
||||
.attachments {
|
||||
max-height: 26rem;
|
||||
min-height: 5rem;
|
||||
|
||||
@ -25,6 +25,7 @@ import { selectCurrentLimit } from '../../../global/selectors/limits';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||
import { validateFiles } from '../../../util/files';
|
||||
import { formatStarsAsIcon } from '../../../util/localization/format';
|
||||
import { removeAllSelections } from '../../../util/selection';
|
||||
import { openSystemFilesDialog } from '../../../util/systemFilesDialog';
|
||||
import getFilesFromDataTransferItems from './helpers/getFilesFromDataTransferItems';
|
||||
@ -36,6 +37,7 @@ import useDerivedState from '../../../hooks/useDerivedState';
|
||||
import useEffectOnce from '../../../hooks/useEffectOnce';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useGetSelectionRange from '../../../hooks/useGetSelectionRange';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated';
|
||||
@ -87,7 +89,9 @@ export type OwnProps = {
|
||||
onRemoveSymbol: VoidFunction;
|
||||
onEmojiSelect: (emoji: string) => void;
|
||||
canScheduleUntilOnline?: boolean;
|
||||
canSchedule?: boolean;
|
||||
onSendWhenOnline?: NoneToVoidFunction;
|
||||
paidMessagesStars?: number;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -144,7 +148,9 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
onRemoveSymbol,
|
||||
onEmojiSelect,
|
||||
canScheduleUntilOnline,
|
||||
canSchedule,
|
||||
onSendWhenOnline,
|
||||
paidMessagesStars,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@ -152,7 +158,8 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
const { addRecentCustomEmoji, addRecentEmoji, updateAttachmentSettings } = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const mainButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
@ -426,7 +433,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
requestMutation(() => {
|
||||
input.style.setProperty('--margin-for-scrollbar', `${width}px`);
|
||||
});
|
||||
}, [lang, isOpen]);
|
||||
}, [oldLang, isOpen]);
|
||||
|
||||
const MoreMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
||||
return ({ onTrigger, isOpen: isMenuOpen }) => (
|
||||
@ -481,14 +488,15 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
})();
|
||||
|
||||
let title = '';
|
||||
const attachmentsLength = renderingAttachments.length;
|
||||
if (areAllPhotos) {
|
||||
title = lang(isEditing ? 'EditMessageReplacePhoto' : 'PreviewSender.SendPhoto', renderingAttachments.length, 'i');
|
||||
title = oldLang(isEditing ? 'EditMessageReplacePhoto' : 'PreviewSender.SendPhoto', attachmentsLength, 'i');
|
||||
} else if (areAllVideos) {
|
||||
title = lang(isEditing ? 'EditMessageReplaceVideo' : 'PreviewSender.SendVideo', renderingAttachments.length, 'i');
|
||||
title = oldLang(isEditing ? 'EditMessageReplaceVideo' : 'PreviewSender.SendVideo', attachmentsLength, 'i');
|
||||
} else if (areAllAudios) {
|
||||
title = lang(isEditing ? 'EditMessageReplaceAudio' : 'PreviewSender.SendAudio', renderingAttachments.length, 'i');
|
||||
title = oldLang(isEditing ? 'EditMessageReplaceAudio' : 'PreviewSender.SendAudio', attachmentsLength, 'i');
|
||||
} else {
|
||||
title = lang(isEditing ? 'EditMessageReplaceFile' : 'PreviewSender.SendFile', renderingAttachments.length, 'i');
|
||||
title = oldLang(isEditing ? 'EditMessageReplaceFile' : 'PreviewSender.SendFile', attachmentsLength, 'i');
|
||||
}
|
||||
|
||||
function renderHeader() {
|
||||
@ -497,7 +505,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="modal-header-condensed" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<div className="modal-header-condensed" dir={oldLang.isRtl ? 'rtl' : undefined}>
|
||||
<Button round color="translucent" size="smaller" ariaLabel="Cancel attachments" onClick={onClear}>
|
||||
<Icon name="close" />
|
||||
</Button>
|
||||
@ -510,7 +518,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
positionX="right"
|
||||
>
|
||||
{Boolean(!editingMessage) && (
|
||||
<MenuItem icon="add" onClick={handleDocumentSelect}>{lang('Add')}</MenuItem>
|
||||
<MenuItem icon="add" onClick={handleDocumentSelect}>{oldLang('Add')}</MenuItem>
|
||||
)}
|
||||
{hasMedia && (
|
||||
<>
|
||||
@ -518,12 +526,12 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
canInvertMedia && (!isInvertedMedia ? (
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
<MenuItem icon="move-caption-up" onClick={() => setIsInvertedMedia(true)}>
|
||||
{lang('PreviewSender.MoveTextUp')}
|
||||
{oldLang('PreviewSender.MoveTextUp')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
<MenuItem icon="move-caption-down" onClick={() => setIsInvertedMedia(undefined)}>
|
||||
{lang(('PreviewSender.MoveTextDown'))}
|
||||
{oldLang(('PreviewSender.MoveTextDown'))}
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
@ -531,7 +539,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
!shouldForceAsFile && !shouldForceCompression && (isSendingCompressed ? (
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
<MenuItem icon="document" onClick={() => setShouldSendCompressed(false)}>
|
||||
{lang(isMultiple ? 'Attachment.SendAsFiles' : 'Attachment.SendAsFile')}
|
||||
{oldLang(isMultiple ? 'Attachment.SendAsFiles' : 'Attachment.SendAsFile')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
@ -543,11 +551,11 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
{isSendingCompressed && hasAnySpoilerable && Boolean(!editingMessage) && (
|
||||
hasSpoiler ? (
|
||||
<MenuItem icon="spoiler-disable" onClick={handleDisableSpoilers}>
|
||||
{lang('Attachment.DisableSpoiler')}
|
||||
{oldLang('Attachment.DisableSpoiler')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem icon="spoiler" onClick={handleEnableSpoilers}>
|
||||
{lang('Attachment.EnableSpoiler')}
|
||||
{oldLang('Attachment.EnableSpoiler')}
|
||||
</MenuItem>
|
||||
)
|
||||
)}
|
||||
@ -576,6 +584,12 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
const isBottomDividerShown = !areAttachmentsScrolledToBottom || !isCaptionNotScrolled;
|
||||
const buttonSendCaption = paidMessagesStars ? formatStarsAsIcon(lang,
|
||||
attachmentsLength * paidMessagesStars,
|
||||
{
|
||||
className: styles.sendButtonStar,
|
||||
asFont: true,
|
||||
}) : oldLang('Send');
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@ -591,6 +605,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
forceDarkTheme && 'component-theme-dark',
|
||||
)}
|
||||
noBackdropClose
|
||||
isLowStackPriority
|
||||
>
|
||||
<div
|
||||
className={styles.dropTarget}
|
||||
@ -599,7 +614,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={unmarkHovered}
|
||||
data-attach-description={lang('Preview.Dragging.AddItems', 10)}
|
||||
data-attach-description={oldLang('Preview.Dragging.AddItems', 10)}
|
||||
data-dropzone
|
||||
>
|
||||
<svg className={styles.dropOutlineContainer}>
|
||||
@ -684,7 +699,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
isActive={isOpen}
|
||||
getHtml={getHtml}
|
||||
editableInputId={EDITABLE_INPUT_MODAL_ID}
|
||||
placeholder={lang('AddCaption')}
|
||||
placeholder={oldLang('AddCaption')}
|
||||
onUpdate={onCaptionUpdate}
|
||||
onSend={handleSendClick}
|
||||
onScroll={handleCaptionScroll}
|
||||
@ -700,12 +715,13 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
onClick={handleSendClick}
|
||||
onContextMenu={canShowCustomSendMenu ? handleContextMenu : undefined}
|
||||
>
|
||||
{shouldSchedule && !editingMessage ? lang('Next') : editingMessage ? lang('Save') : lang('Send')}
|
||||
{shouldSchedule && !editingMessage ? oldLang('Next')
|
||||
: editingMessage ? oldLang('Save') : buttonSendCaption}
|
||||
</Button>
|
||||
{canShowCustomSendMenu && (
|
||||
<CustomSendMenu
|
||||
isOpen={isCustomSendMenuOpen}
|
||||
canSchedule={isForMessage}
|
||||
canSchedule={canSchedule && isForMessage}
|
||||
onSendSilent={!isChatWithSelf ? handleSendSilent : undefined}
|
||||
onSendSchedule={handleScheduleClick}
|
||||
onClose={handleContextMenuClose}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { ChangeEvent, RefObject } from 'react';
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { FC, TeactNode } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
getIsHeavyAnimating,
|
||||
memo, useEffect, useLayoutEffect,
|
||||
@ -58,7 +58,7 @@ type OwnProps = {
|
||||
isReady: boolean;
|
||||
isActive: boolean;
|
||||
getHtml: Signal<string>;
|
||||
placeholder: string;
|
||||
placeholder: TeactNode | string;
|
||||
timedPlaceholderLangKey?: string;
|
||||
timedPlaceholderDate?: number;
|
||||
forcedPlaceholder?: string;
|
||||
@ -168,7 +168,7 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const absoluteContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const lang = useOldLang();
|
||||
const oldLang = useOldLang();
|
||||
const isContextMenuOpenRef = useRef(false);
|
||||
const [isTextFormatterOpen, openTextFormatter, closeTextFormatter] = useFlag();
|
||||
const [textFormatterAnchorPosition, setTextFormatterAnchorPosition] = useState<IAnchorPosition>();
|
||||
@ -561,9 +561,10 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
|
||||
const inputScrollerContentClass = buildClassName('input-scroller-content', isNeedPremium && 'is-need-premium');
|
||||
const placeholderAriaLabel = typeof placeholder === 'string' ? placeholder : undefined;
|
||||
|
||||
return (
|
||||
<div id={id} onClick={shouldSuppressFocus ? onSuppressedFocus : undefined} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<div id={id} onClick={shouldSuppressFocus ? onSuppressedFocus : undefined} dir={oldLang.isRtl ? 'rtl' : undefined}>
|
||||
<div
|
||||
className={buildClassName('custom-scroll', SCROLLER_CLASS, isNeedPremium && 'is-need-premium')}
|
||||
onScroll={onScroll}
|
||||
@ -584,7 +585,7 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
||||
onMouseDown={handleMouseDown}
|
||||
onContextMenu={IS_ANDROID ? handleAndroidContextMenu : undefined}
|
||||
onTouchCancel={IS_ANDROID ? processSelectionWithTimeout : undefined}
|
||||
aria-label={placeholder}
|
||||
aria-label={placeholderAriaLabel}
|
||||
onFocus={!isNeedPremium ? onFocus : undefined}
|
||||
onBlur={!isNeedPremium ? onBlur : undefined}
|
||||
/>
|
||||
@ -604,7 +605,7 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
||||
) : placeholder}
|
||||
{isStoryInput && isNeedPremium && (
|
||||
<Button className="unlock-button" size="tiny" color="adaptive" onClick={handleOpenPremiumModal}>
|
||||
{lang('StoryRepliesLockedButton')}
|
||||
{oldLang('StoryRepliesLockedButton')}
|
||||
</Button>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
import { useRef, useState } from '../../../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../../../global';
|
||||
|
||||
import { PAID_MESSAGES_PURPOSE } from '../../../../config';
|
||||
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
|
||||
export default function usePaidMessageConfirmation(
|
||||
starsForAllMessages: number,
|
||||
) {
|
||||
const {
|
||||
shouldPaidMessageAutoApprove,
|
||||
} = getGlobal().settings.byKey;
|
||||
|
||||
const [shouldAutoApprove,
|
||||
setAutoApprove] = useState(Boolean(shouldPaidMessageAutoApprove));
|
||||
const confirmPaymentHandlerRef = useRef<NoneToVoidFunction | undefined>(undefined);
|
||||
|
||||
const closeConfirmDialog = useLastCallback(() => {
|
||||
getActions().closePaymentMessageConfirmDialogOpen();
|
||||
});
|
||||
|
||||
const handleWithConfirmation = <T extends (...args: any[]) => void>(
|
||||
handler: T,
|
||||
...args: Parameters<T>
|
||||
) => {
|
||||
if (starsForAllMessages) {
|
||||
const balance = getGlobal().stars?.balance.amount;
|
||||
if (balance && starsForAllMessages > balance) {
|
||||
getActions().openStarsBalanceModal({
|
||||
topup:
|
||||
{ balanceNeeded: starsForAllMessages, purpose: PAID_MESSAGES_PURPOSE },
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldPaidMessageAutoApprove && starsForAllMessages) {
|
||||
confirmPaymentHandlerRef.current = () => handler(...args);
|
||||
getActions().openPaymentMessageConfirmDialogOpen();
|
||||
} else {
|
||||
handler(...args);
|
||||
}
|
||||
};
|
||||
|
||||
const dialogHandler = useLastCallback(() => {
|
||||
confirmPaymentHandlerRef.current?.();
|
||||
getActions().closePaymentMessageConfirmDialogOpen();
|
||||
if (shouldAutoApprove) getActions().setPaidMessageAutoApprove();
|
||||
});
|
||||
|
||||
return {
|
||||
closeConfirmDialog,
|
||||
handleWithConfirmation,
|
||||
dialogHandler,
|
||||
shouldAutoApprove,
|
||||
setAutoApprove,
|
||||
};
|
||||
}
|
||||
@ -90,6 +90,7 @@ export function groupMessages(
|
||||
} else if (
|
||||
nextMessage.id === firstUnreadId
|
||||
|| message.senderId !== nextMessage.senderId
|
||||
|| message.paidMessageStars
|
||||
|| message.isOutgoing !== nextMessage.isOutgoing
|
||||
|| message.postAuthorTitle !== nextMessage.postAuthorTitle
|
||||
|| (isActionMessage(message) && message.content.action?.type !== 'phoneCall')
|
||||
|
||||
@ -301,6 +301,8 @@ type StateProps = {
|
||||
poll?: ApiPoll;
|
||||
maxTimestamp?: number;
|
||||
lastPlaybackTimestamp?: number;
|
||||
paidMessageStars?: number;
|
||||
isChatWithUser?: boolean;
|
||||
};
|
||||
|
||||
type MetaPosition =
|
||||
@ -421,6 +423,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
maxTimestamp,
|
||||
lastPlaybackTimestamp,
|
||||
onIntersectPinnedMessage,
|
||||
paidMessageStars,
|
||||
isChatWithUser,
|
||||
}) => {
|
||||
const {
|
||||
toggleMessageSelection,
|
||||
@ -787,6 +791,10 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
const withAppendix = contentClassName.includes('has-appendix');
|
||||
const emojiSize = getCustomEmojiSize(message.emojiOnlyCount);
|
||||
|
||||
const paidMessageStarsInMeta = !isChatWithUser
|
||||
? (isAlbum && paidMessageStars ? album.messages.length * paidMessageStars : paidMessageStars)
|
||||
: undefined;
|
||||
|
||||
let metaPosition!: MetaPosition;
|
||||
if (phoneCall) {
|
||||
metaPosition = 'none';
|
||||
@ -1019,6 +1027,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
onEffectClick={handleEffectClick}
|
||||
onTranslationClick={handleTranslationClick}
|
||||
onOpenThread={handleOpenThread}
|
||||
paidMessageStars={paidMessageStarsInMeta}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -1119,7 +1128,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
{hasAnimatedEmoji && animatedCustomEmoji && (
|
||||
<AnimatedCustomEmoji
|
||||
customEmojiId={animatedCustomEmoji}
|
||||
withEffects={withAnimatedEffects && isUserId(chatId) && !effect}
|
||||
withEffects={withAnimatedEffects && isChatWithUser && !effect}
|
||||
isOwn={isOwn}
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
forceLoadPreview={isLocal}
|
||||
@ -1131,7 +1140,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
{hasAnimatedEmoji && animatedEmoji && (
|
||||
<AnimatedEmoji
|
||||
emoji={animatedEmoji}
|
||||
withEffects={withAnimatedEffects && isUserId(chatId) && !effect}
|
||||
withEffects={withAnimatedEffects && isChatWithUser && !effect}
|
||||
isOwn={isOwn}
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
forceLoadPreview={isLocal}
|
||||
@ -1719,15 +1728,18 @@ export default memo(withGlobal<OwnProps>(
|
||||
} = ownProps;
|
||||
const {
|
||||
id, chatId, viaBotId, isOutgoing, forwardInfo, transcriptionId, isPinned, viaBusinessBotId, effectId,
|
||||
paidMessageStars,
|
||||
} = message;
|
||||
|
||||
const isChatWithUser = isUserId(chatId);
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
|
||||
const isSystemBotChat = isSystemBot(chatId);
|
||||
const isAnonymousForwards = isAnonymousForwardsChat(chatId);
|
||||
const isChannel = chat && isChatChannel(chat);
|
||||
const isGroup = chat && isChatGroup(chat);
|
||||
const chatFullInfo = !isUserId(chatId) ? selectChatFullInfo(global, chatId) : undefined;
|
||||
const chatFullInfo = !isChatWithUser ? selectChatFullInfo(global, chatId) : undefined;
|
||||
const webPageStoryData = message.content.webPage?.story;
|
||||
const webPageStory = webPageStoryData
|
||||
? selectPeerStory(global, webPageStoryData.peerId, webPageStoryData.id)
|
||||
@ -1931,6 +1943,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
poll,
|
||||
maxTimestamp,
|
||||
lastPlaybackTimestamp,
|
||||
paidMessageStars,
|
||||
isChatWithUser,
|
||||
};
|
||||
},
|
||||
)(Message));
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
user-select: none;
|
||||
|
||||
.message-price,
|
||||
.message-time,
|
||||
.message-imported,
|
||||
.message-signature,
|
||||
@ -26,6 +27,22 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.message-price-stars-container {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.message-price-star-icon {
|
||||
margin-inline-start: 0 !important;
|
||||
margin-inline-end: 0.0625rem !important;
|
||||
}
|
||||
|
||||
.message-price {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-inline-end: 0.25rem;
|
||||
}
|
||||
|
||||
.message-replies-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -8,6 +8,7 @@ import type {
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatDateTimeToString, formatPastTimeShort, formatTime } from '../../../util/dates/dateFormat';
|
||||
import { formatStarsAsIcon } from '../../../util/localization/format';
|
||||
import { formatIntegerCompact } from '../../../util/textFormat';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
@ -38,6 +39,7 @@ type OwnProps = {
|
||||
onEffectClick: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
renderQuickReactionButton?: () => TeactNode | undefined;
|
||||
onOpenThread: NoneToVoidFunction;
|
||||
paidMessageStars?: number;
|
||||
};
|
||||
|
||||
const MessageMeta: FC<OwnProps> = ({
|
||||
@ -56,6 +58,7 @@ const MessageMeta: FC<OwnProps> = ({
|
||||
onTranslationClick,
|
||||
onEffectClick,
|
||||
onOpenThread,
|
||||
paidMessageStars,
|
||||
}) => {
|
||||
const { showNotification } = getActions();
|
||||
|
||||
@ -176,6 +179,16 @@ const MessageMeta: FC<OwnProps> = ({
|
||||
{signature && (
|
||||
<span className="message-signature">{renderText(signature)}</span>
|
||||
)}
|
||||
{paidMessageStars && (
|
||||
<span className="message-price">{
|
||||
formatStarsAsIcon(lang, paidMessageStars, {
|
||||
asFont: true,
|
||||
className: 'message-price-star-icon',
|
||||
containerClassName: 'message-price-stars-container',
|
||||
})
|
||||
}
|
||||
</span>
|
||||
)}
|
||||
<span className="message-time" title={dateTitle} onMouseEnter={markActivated}>
|
||||
{message.forwardInfo?.isImported && (
|
||||
<>
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
@use "../../../styles/mixins";
|
||||
|
||||
.root {
|
||||
@include mixins.header-pane;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
padding-inline: 1rem;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.375rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.messageStars {
|
||||
font-weight: var(--font-weight-medium);
|
||||
padding-inline: 0.25rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.messageStarIcon {
|
||||
margin-inline-start: 0 !important;
|
||||
margin-inline-end: 0.125rem !important;
|
||||
}
|
||||
|
||||
.checkBox {
|
||||
margin-top: 0.375rem;
|
||||
margin-inline: -1.125rem;
|
||||
padding-inline-start: 3.5rem;
|
||||
}
|
||||
144
src/components/middle/panes/PaidMessageChargePane.tsx
Normal file
144
src/components/middle/panes/PaidMessageChargePane.tsx
Normal file
@ -0,0 +1,144 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type {
|
||||
ApiChat,
|
||||
} from '../../../api/types';
|
||||
|
||||
import {
|
||||
getPeerTitle,
|
||||
} from '../../../global/helpers';
|
||||
import {
|
||||
selectChat,
|
||||
selectUserFullInfo,
|
||||
} from '../../../global/selectors';
|
||||
import { formatStarsAsIcon } from '../../../util/localization/format';
|
||||
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
// import useTimeout from '../../../hooks/schedulers/useTimeout';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useHeaderPane, { type PaneState } from '../hooks/useHeaderPane';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import Checkbox from '../../ui/Checkbox';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
|
||||
// import CustomEmoji from '../../common/CustomEmoji';
|
||||
import styles from './PaidMessageChargePane.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
peerId: string;
|
||||
onPaneStateChange?: (state: PaneState) => void;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
chargedPaidMessageStars?: number;
|
||||
chat?: ApiChat;
|
||||
};
|
||||
|
||||
const PaidMessageChargePane: FC<OwnProps & StateProps> = ({
|
||||
chargedPaidMessageStars,
|
||||
chat,
|
||||
onPaneStateChange,
|
||||
peerId,
|
||||
}) => {
|
||||
const isOpen = Boolean(chargedPaidMessageStars);
|
||||
const lang = useLang();
|
||||
const [isRemoveFeeDialogOpen, openRemoveFeeDialog, closeRemoveFeeDialog] = useFlag();
|
||||
const [shouldRefoundStars, setShouldRefoundStars] = useFlag(false);
|
||||
|
||||
const {
|
||||
addNoPaidMessagesException,
|
||||
} = getActions();
|
||||
|
||||
const { ref, shouldRender } = useHeaderPane({
|
||||
isOpen,
|
||||
onStateChange: onPaneStateChange,
|
||||
});
|
||||
|
||||
const handleRemoveFee = useLastCallback(() => {
|
||||
openRemoveFeeDialog();
|
||||
});
|
||||
|
||||
const handleConfirmRemoveFee = useLastCallback(() => {
|
||||
addNoPaidMessagesException({ userId: peerId, shouldRefundCharged: shouldRefoundStars });
|
||||
});
|
||||
|
||||
if (!shouldRender || !chargedPaidMessageStars) return undefined;
|
||||
|
||||
const peerName = chat ? getPeerTitle(lang, chat) : undefined;
|
||||
|
||||
const message = lang('PaneMessagePaidMessageCharge', {
|
||||
peer: peerName,
|
||||
amount: formatStarsAsIcon(lang,
|
||||
chargedPaidMessageStars,
|
||||
{ asFont: true, className: styles.messageStarIcon, containerClassName: styles.messageStars }),
|
||||
}, {
|
||||
withMarkdown: true,
|
||||
withNodes: true,
|
||||
});
|
||||
|
||||
const dialogMessage = lang('ConfirmDialogMessageRemoveFee', {
|
||||
peer: peerName,
|
||||
}, {
|
||||
withMarkdown: true,
|
||||
withNodes: true,
|
||||
});
|
||||
|
||||
const checkBoxTitle = lang('ConfirmDialogRemoveFeeRefundStars', {
|
||||
amount: chargedPaidMessageStars,
|
||||
}, {
|
||||
withMarkdown: true,
|
||||
withNodes: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={ref} className={styles.root}>
|
||||
<div className={styles.message}>
|
||||
{message}
|
||||
</div>
|
||||
<Button
|
||||
isText
|
||||
noForcedUpperCase
|
||||
pill
|
||||
fluid
|
||||
size="tiny"
|
||||
className={styles.button}
|
||||
onClick={handleRemoveFee}
|
||||
>
|
||||
{lang('RemoveFeeTitle')}
|
||||
</Button>
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={isRemoveFeeDialogOpen}
|
||||
onClose={closeRemoveFeeDialog}
|
||||
title={lang('RemoveFeeTitle')}
|
||||
confirmLabel={lang('ConfirmRemoveMessageFee')}
|
||||
confirmHandler={handleConfirmRemoveFee}
|
||||
>
|
||||
{dialogMessage}
|
||||
<Checkbox
|
||||
className={styles.checkBox}
|
||||
label={checkBoxTitle}
|
||||
checked={shouldRefoundStars}
|
||||
onCheck={setShouldRefoundStars}
|
||||
/>
|
||||
</ConfirmDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { peerId }): StateProps => {
|
||||
const chat = selectChat(global, peerId);
|
||||
const peerFullInfo = selectUserFullInfo(global, peerId);
|
||||
const chargedPaidMessageStars = peerFullInfo?.settings?.chargedPaidMessageStars;
|
||||
|
||||
return {
|
||||
chargedPaidMessageStars,
|
||||
chat,
|
||||
};
|
||||
},
|
||||
)(PaidMessageChargePane));
|
||||
@ -10,9 +10,14 @@ import {
|
||||
type ApiMessage, type ApiPeer, type ApiStarsAmount, MAIN_THREAD_ID,
|
||||
} from '../../../api/types';
|
||||
|
||||
import { getPeerTitle } from '../../../global/helpers';
|
||||
import {
|
||||
getPeerTitle,
|
||||
} from '../../../global/helpers';
|
||||
import { isApiPeerUser } from '../../../global/helpers/peers';
|
||||
import { selectPeer, selectTabState, selectTheme } from '../../../global/selectors';
|
||||
import {
|
||||
selectPeer, selectPeerPaidMessagesStars,
|
||||
selectTabState, selectTheme,
|
||||
} from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import buildStyle from '../../../util/buildStyle';
|
||||
import { formatCurrency } from '../../../util/formatCurrency';
|
||||
@ -49,6 +54,7 @@ export type StateProps = {
|
||||
currentUserId?: string;
|
||||
isPaymentFormLoading?: boolean;
|
||||
starBalance?: ApiStarsAmount;
|
||||
paidMessagesStars?: number;
|
||||
};
|
||||
|
||||
const LIMIT_DISPLAY_THRESHOLD = 50;
|
||||
@ -67,6 +73,7 @@ function GiftComposer({
|
||||
currentUserId,
|
||||
isPaymentFormLoading,
|
||||
starBalance,
|
||||
paidMessagesStars,
|
||||
}: OwnProps & StateProps) {
|
||||
const {
|
||||
sendStarGift, sendPremiumGiftByStars, openInvoice, openGiftUpgradeModal, openStarsBalanceModal,
|
||||
@ -202,14 +209,19 @@ function GiftComposer({
|
||||
const title = getPeerTitle(lang, peer!)!;
|
||||
return (
|
||||
<div className={styles.optionsSection}>
|
||||
<TextArea
|
||||
className={styles.messageInput}
|
||||
onChange={handleGiftMessageChange}
|
||||
value={giftMessage}
|
||||
label={lang('GiftMessagePlaceholder')}
|
||||
maxLength={captionLimit}
|
||||
maxLengthIndicator={symbolsLeft && symbolsLeft < LIMIT_DISPLAY_THRESHOLD ? symbolsLeft.toString() : undefined}
|
||||
/>
|
||||
|
||||
{!paidMessagesStars && (
|
||||
<TextArea
|
||||
className={styles.messageInput}
|
||||
onChange={handleGiftMessageChange}
|
||||
value={giftMessage}
|
||||
label={lang('GiftMessagePlaceholder')}
|
||||
maxLength={captionLimit}
|
||||
maxLengthIndicator={
|
||||
symbolsLeft && symbolsLeft < LIMIT_DISPLAY_THRESHOLD ? symbolsLeft.toString() : undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{canUseStarsPayment && (
|
||||
<ListItem className={styles.switcher} narrow ripple onClick={toggleShouldPayByStars}>
|
||||
@ -377,6 +389,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
backgroundColor,
|
||||
} = global.settings.themes[theme] || {};
|
||||
const peer = selectPeer(global, peerId);
|
||||
const paidMessagesStars = selectPeerPaidMessagesStars(global, peerId);
|
||||
|
||||
const tabState = selectTabState(global);
|
||||
|
||||
@ -391,6 +404,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
captionLimit: global.appConfig?.starGiftMaxMessageLength,
|
||||
currentUserId: global.currentUserId,
|
||||
isPaymentFormLoading: tabState.isPaymentFormLoading,
|
||||
paidMessagesStars,
|
||||
};
|
||||
},
|
||||
)(GiftComposer));
|
||||
|
||||
@ -55,7 +55,7 @@
|
||||
}
|
||||
|
||||
.giftTitle {
|
||||
font-weight: 500;
|
||||
font-weight: var(--font-weight-medium);
|
||||
font-size: 1.5rem;
|
||||
text-align: center;
|
||||
padding-bottom: 0.5rem;
|
||||
|
||||
@ -2,45 +2,78 @@ import React, {
|
||||
type FC,
|
||||
memo, useEffect,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../../global';
|
||||
import {
|
||||
getActions, getGlobal, withGlobal,
|
||||
} from '../../../global';
|
||||
|
||||
import type { TabState } from '../../../global/types';
|
||||
import type { ThreadId } from '../../../types';
|
||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
|
||||
import { getPeerTitle } from '../../../global/helpers';
|
||||
import { selectPeer } from '../../../global/selectors';
|
||||
import {
|
||||
getPeerTitle,
|
||||
} from '../../../global/helpers';
|
||||
import {
|
||||
selectPeer, selectTabState,
|
||||
} from '../../../global/selectors';
|
||||
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import usePaidMessageConfirmation from '../../middle/composer/hooks/usePaidMessageConfirmation';
|
||||
|
||||
import PaymentMessageConfirmDialog from '../../common/PaymentMessageConfirmDialog';
|
||||
import RecipientPicker from '../../common/RecipientPicker';
|
||||
|
||||
export type OwnProps = {
|
||||
modal: TabState['sharePreparedMessageModal'];
|
||||
};
|
||||
|
||||
const SharePreparedMessageModal: FC<OwnProps> = ({
|
||||
modal,
|
||||
type StateProps = {
|
||||
isPaymentMessageConfirmDialogOpen: boolean;
|
||||
};
|
||||
|
||||
export type SendParams = {
|
||||
peerName?: string;
|
||||
starsForSendMessage: number;
|
||||
};
|
||||
|
||||
const SharePreparedMessageModal: FC<OwnProps & StateProps> = ({
|
||||
modal, isPaymentMessageConfirmDialogOpen,
|
||||
}) => {
|
||||
const {
|
||||
closeSharePreparedMessageModal,
|
||||
sendInlineBotResult,
|
||||
sendWebAppEvent,
|
||||
showNotification,
|
||||
updateSharePreparedMessageModalSendArgs,
|
||||
} = getActions();
|
||||
const lang = useOldLang();
|
||||
const isOpen = Boolean(modal);
|
||||
|
||||
const [isShown, markIsShown, unmarkIsShown] = useFlag();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
markIsShown();
|
||||
}
|
||||
}, [isOpen, markIsShown]);
|
||||
|
||||
const { message, filter, webAppKey } = modal || {};
|
||||
const {
|
||||
message, filter, webAppKey, pendingSendArgs,
|
||||
} = modal || {};
|
||||
|
||||
const {
|
||||
starsForSendMessage,
|
||||
} = pendingSendArgs || {};
|
||||
|
||||
const {
|
||||
closeConfirmDialog: closeConfirmModalPayForMessage,
|
||||
dialogHandler: paymentMessageConfirmDialogHandler,
|
||||
shouldAutoApprove: shouldPaidMessageAutoApprove,
|
||||
setAutoApprove: setShouldPaidMessageAutoApprove,
|
||||
handleWithConfirmation: handleActionWithPaymentConfirmation,
|
||||
} = usePaidMessageConfirmation(starsForSendMessage || 0);
|
||||
|
||||
const handleClose = useLastCallback(() => {
|
||||
closeSharePreparedMessageModal();
|
||||
@ -55,7 +88,7 @@ const SharePreparedMessageModal: FC<OwnProps> = ({
|
||||
}
|
||||
});
|
||||
|
||||
const handleSelectRecipient = useLastCallback((id: string, threadId?: ThreadId) => {
|
||||
const handleSend = useLastCallback((id: string, threadId?: ThreadId) => {
|
||||
if (message && webAppKey) {
|
||||
const global = getGlobal();
|
||||
const peer = selectPeer(global, id);
|
||||
@ -65,33 +98,82 @@ const SharePreparedMessageModal: FC<OwnProps> = ({
|
||||
id: message.result.id,
|
||||
queryId: message.result.queryId,
|
||||
});
|
||||
if (!starsForSendMessage) {
|
||||
showNotification({
|
||||
message: lang('BotSharedToOne', getPeerTitle(lang, peer!)),
|
||||
});
|
||||
}
|
||||
sendWebAppEvent({
|
||||
webAppKey,
|
||||
event: {
|
||||
eventType: 'prepared_message_sent',
|
||||
},
|
||||
});
|
||||
showNotification({
|
||||
message: lang('BotSharedToOne', getPeerTitle(lang, peer!)),
|
||||
});
|
||||
closeSharePreparedMessageModal();
|
||||
updateSharePreparedMessageModalSendArgs({ args: undefined });
|
||||
}
|
||||
});
|
||||
|
||||
const handleSelectRecipient = useLastCallback((id: string, threadId?: ThreadId) => {
|
||||
updateSharePreparedMessageModalSendArgs({ args: { peerId: id, threadId } });
|
||||
});
|
||||
|
||||
const handleSendWithPaymentConformation = useLastCallback(() => {
|
||||
if (pendingSendArgs) {
|
||||
handleActionWithPaymentConfirmation(handleSend, pendingSendArgs.peerId, pendingSendArgs.threadId);
|
||||
}
|
||||
});
|
||||
|
||||
const handleClosePaymentMessageConfirmDialog = useLastCallback(() => {
|
||||
closeConfirmModalPayForMessage();
|
||||
updateSharePreparedMessageModalSendArgs({ args: undefined });
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (pendingSendArgs) {
|
||||
handleSendWithPaymentConformation();
|
||||
}
|
||||
}, [pendingSendArgs]);
|
||||
|
||||
const global = getGlobal();
|
||||
const peer = pendingSendArgs ? selectPeer(global, pendingSendArgs.peerId) : undefined;
|
||||
const peerName = peer ? getPeerTitle(lang, peer) : undefined;
|
||||
|
||||
if (!isOpen && !isShown) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<RecipientPicker
|
||||
isOpen={isOpen}
|
||||
searchPlaceholder={lang('Search')}
|
||||
filter={filter}
|
||||
onSelectRecipient={handleSelectRecipient}
|
||||
onClose={handleClose}
|
||||
onCloseAnimationEnd={unmarkIsShown}
|
||||
/>
|
||||
<>
|
||||
<RecipientPicker
|
||||
isOpen={isOpen}
|
||||
searchPlaceholder={lang('Search')}
|
||||
filter={filter}
|
||||
onSelectRecipient={handleSelectRecipient}
|
||||
onClose={handleClose}
|
||||
onCloseAnimationEnd={unmarkIsShown}
|
||||
isLowStackPriority
|
||||
/>
|
||||
<PaymentMessageConfirmDialog
|
||||
isOpen={isPaymentMessageConfirmDialogOpen}
|
||||
onClose={handleClosePaymentMessageConfirmDialog}
|
||||
userName={peerName}
|
||||
messagePriceInStars={starsForSendMessage || 0}
|
||||
messagesCount={1}
|
||||
shouldAutoApprove={shouldPaidMessageAutoApprove}
|
||||
setAutoApprove={setShouldPaidMessageAutoApprove}
|
||||
confirmHandler={paymentMessageConfirmDialogHandler}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SharePreparedMessageModal);
|
||||
export default memo(withGlobal(
|
||||
(global): StateProps => {
|
||||
const tabState = selectTabState(global);
|
||||
const { isPaymentMessageConfirmDialogOpen } = tabState;
|
||||
return {
|
||||
isPaymentMessageConfirmDialogOpen,
|
||||
};
|
||||
},
|
||||
)(SharePreparedMessageModal));
|
||||
|
||||
@ -7,6 +7,7 @@ import type { ApiStarTopupOption } from '../../../api/types';
|
||||
import type { GlobalState, TabState } from '../../../global/types';
|
||||
import type { RegularLangKey } from '../../../types/language';
|
||||
|
||||
import { PAID_MESSAGES_PURPOSE } from '../../../config';
|
||||
import { getChatTitle, getPeerTitle, getUserFullName } from '../../../global/helpers';
|
||||
import { selectChat, selectIsPremiumPurchaseBlocked, selectUser } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
@ -108,6 +109,13 @@ const StarsBalanceModal = ({
|
||||
return oldLang('StarsNeededTextLink');
|
||||
}
|
||||
|
||||
if (topup?.purpose === PAID_MESSAGES_PURPOSE) {
|
||||
return lang('StarsNeededTextSendPaidMessages', undefined, {
|
||||
withMarkdown: true,
|
||||
withNodes: true,
|
||||
});
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [originReaction, originStarsPayment, originGift, topup?.purpose, lang, oldLang]);
|
||||
|
||||
|
||||
@ -2,27 +2,40 @@ import type { ApiStarsAmount, ApiStarsTransaction } from '../../../../api/types'
|
||||
import type { OldLangFn } from '../../../../hooks/useOldLang';
|
||||
|
||||
import { buildStarsTransactionCustomPeer } from '../../../../global/helpers/payments';
|
||||
import {
|
||||
type LangFn,
|
||||
} from '../../../../util/localization';
|
||||
import { formatPercent } from '../../../../util/textFormat';
|
||||
|
||||
export function getTransactionTitle(lang: OldLangFn, transaction: ApiStarsTransaction) {
|
||||
if (transaction.starRefCommision) {
|
||||
return lang('StarTransactionCommission', formatPercent(transaction.starRefCommision));
|
||||
export function getTransactionTitle(oldLang: OldLangFn, lang: LangFn, transaction: ApiStarsTransaction) {
|
||||
if (transaction.paidMessages) {
|
||||
return lang(
|
||||
'PaidMessageTransaction',
|
||||
{ count: transaction.paidMessages },
|
||||
{
|
||||
withNodes: true,
|
||||
pluralValue: transaction.paidMessages,
|
||||
},
|
||||
);
|
||||
}
|
||||
if (transaction.isGiftUpgrade) return lang('Gift2TransactionUpgraded');
|
||||
if (transaction.extendedMedia) return lang('StarMediaPurchase');
|
||||
if (transaction.subscriptionPeriod) return transaction.title || lang('StarSubscriptionPurchase');
|
||||
if (transaction.isReaction) return lang('StarsReactionsSent');
|
||||
if (transaction.giveawayPostId) return lang('StarsGiveawayPrizeReceived');
|
||||
if (transaction.isMyGift) return lang('StarsGiftSent');
|
||||
if (transaction.isGift) return lang('StarsGiftReceived');
|
||||
if (transaction.starRefCommision) {
|
||||
return oldLang('StarTransactionCommission', formatPercent(transaction.starRefCommision));
|
||||
}
|
||||
if (transaction.isGiftUpgrade) return oldLang('Gift2TransactionUpgraded');
|
||||
if (transaction.extendedMedia) return oldLang('StarMediaPurchase');
|
||||
if (transaction.subscriptionPeriod) return transaction.title || oldLang('StarSubscriptionPurchase');
|
||||
if (transaction.isReaction) return oldLang('StarsReactionsSent');
|
||||
if (transaction.giveawayPostId) return oldLang('StarsGiveawayPrizeReceived');
|
||||
if (transaction.isMyGift) return oldLang('StarsGiftSent');
|
||||
if (transaction.isGift) return oldLang('StarsGiftReceived');
|
||||
if (transaction.starGift) {
|
||||
return isNegativeStarsAmount(transaction.stars) ? lang('Gift2TransactionSent') : lang('Gift2ConvertedTitle');
|
||||
return isNegativeStarsAmount(transaction.stars) ? oldLang('Gift2TransactionSent') : oldLang('Gift2ConvertedTitle');
|
||||
}
|
||||
|
||||
const customPeer = (transaction.peer && transaction.peer.type !== 'peer'
|
||||
&& buildStarsTransactionCustomPeer(transaction.peer)) || undefined;
|
||||
|
||||
if (customPeer) return customPeer.title || lang(customPeer.titleKey!);
|
||||
if (customPeer) return customPeer.title || oldLang(customPeer.titleKey!);
|
||||
|
||||
return transaction.title;
|
||||
}
|
||||
|
||||
@ -64,7 +64,7 @@ const StarsTransactionItem = ({ transaction, className }: OwnProps) => {
|
||||
const giftSticker = starGift && getStickerFromGift(starGift);
|
||||
|
||||
const data = useMemo(() => {
|
||||
let title = getTransactionTitle(oldLang, transaction);
|
||||
let title = getTransactionTitle(oldLang, lang, transaction);
|
||||
let description;
|
||||
let status: string | undefined;
|
||||
let avatarPeer: ApiPeer | CustomPeer | undefined;
|
||||
@ -105,7 +105,7 @@ const StarsTransactionItem = ({ transaction, className }: OwnProps) => {
|
||||
avatarPeer,
|
||||
status,
|
||||
};
|
||||
}, [oldLang, peer, transaction]);
|
||||
}, [oldLang, lang, peer, transaction]);
|
||||
|
||||
const previewContent = useMemo(() => {
|
||||
if (isUniqueGift) {
|
||||
|
||||
@ -64,6 +64,16 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.totalStars {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.starIcon {
|
||||
line-height: 1 !important;
|
||||
margin-inline-start: 0 !important;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-block: 0.5rem;
|
||||
|
||||
@ -10,7 +10,10 @@ import type { TabState } from '../../../../global/types';
|
||||
import { MediaViewerOrigin } from '../../../../types';
|
||||
|
||||
import { getMessageLink } from '../../../../global/helpers';
|
||||
import { buildStarsTransactionCustomPeer, formatStarsTransactionAmount } from '../../../../global/helpers/payments';
|
||||
import {
|
||||
buildStarsTransactionCustomPeer,
|
||||
formatStarsTransactionAmount,
|
||||
} from '../../../../global/helpers/payments';
|
||||
import {
|
||||
selectCanPlayAnimatedEmojis,
|
||||
selectGiftStickerForStars,
|
||||
@ -19,6 +22,8 @@ import {
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { copyTextToClipboard } from '../../../../util/clipboard';
|
||||
import { formatDateTimeToString } from '../../../../util/dates/dateFormat';
|
||||
import { formatStarsAsIcon } from '../../../../util/localization/format';
|
||||
import { formatPercent } from '../../../../util/textFormat';
|
||||
import { getGiftAttributes, getStickerFromGift } from '../../../common/helpers/gifts';
|
||||
import { getTransactionTitle, isNegativeStarsAmount } from '../helpers/transaction';
|
||||
|
||||
@ -48,10 +53,11 @@ type StateProps = {
|
||||
peer?: ApiPeer;
|
||||
canPlayAnimatedEmojis?: boolean;
|
||||
topSticker?: ApiSticker;
|
||||
paidMessageCommission?: number;
|
||||
};
|
||||
|
||||
const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
modal, peer, canPlayAnimatedEmojis, topSticker,
|
||||
modal, peer, canPlayAnimatedEmojis, topSticker, paidMessageCommission,
|
||||
}) => {
|
||||
const { showNotification, openMediaViewer, closeStarsTransactionModal } = getActions();
|
||||
|
||||
@ -90,7 +96,7 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
const peerId = transaction.peer?.type === 'peer' ? transaction.peer.id : undefined;
|
||||
const toName = transaction.peer && oldLang(getStarsPeerTitleKey(transaction.peer));
|
||||
|
||||
const title = getTransactionTitle(oldLang, transaction);
|
||||
const title = getTransactionTitle(oldLang, lang, transaction);
|
||||
|
||||
const messageLink = peer && transaction.messageId && !isGiftUpgrade
|
||||
? getMessageLink(peer, undefined, transaction.messageId) : undefined;
|
||||
@ -163,12 +169,25 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
</span>
|
||||
<StarIcon type="gold" size="middle" />
|
||||
</p>
|
||||
{transaction.paidMessages && transaction.starRefCommision && paidMessageCommission
|
||||
&& (
|
||||
<p className={styles.description}>
|
||||
{lang(
|
||||
'PaidMessageTransactionDescription',
|
||||
{ percent: formatPercent(paidMessageCommission / 10) },
|
||||
{
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
},
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
const tableData: TableData = [];
|
||||
|
||||
if (transaction.starRefCommision) {
|
||||
if (transaction && !transaction.paidMessages) {
|
||||
tableData.push([
|
||||
oldLang('StarsTransaction.StarRefReason.Title'),
|
||||
oldLang('StarsTransaction.StarRefReason.Program'),
|
||||
@ -187,7 +206,7 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
peerLabel = oldLang('Stars.Transaction.GiftFrom');
|
||||
} else if (isNegativeStarsAmount(stars) || transaction.isMyGift) {
|
||||
peerLabel = oldLang('Stars.Transaction.To');
|
||||
} else if (transaction.starRefCommision) {
|
||||
} else if (transaction.starRefCommision && !transaction.paidMessages) {
|
||||
peerLabel = oldLang('StarsTransaction.StarRefReason.Miniapp');
|
||||
} else if (peerId) {
|
||||
peerLabel = oldLang('Star.Transaction.From');
|
||||
@ -200,6 +219,15 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
peerId ? { chatId: peerId } : toName || '',
|
||||
]);
|
||||
|
||||
if (transaction.starRefCommision && transaction.paidMessages) {
|
||||
tableData.push([
|
||||
lang('PaidMessageTransactionTotal'),
|
||||
formatStarsAsIcon(lang,
|
||||
transaction.stars.amount / ((100 - transaction.starRefCommision) / 100),
|
||||
{ asFont: false, className: styles.starIcon, containerClassName: styles.totalStars }),
|
||||
]);
|
||||
}
|
||||
|
||||
if (messageLink) {
|
||||
tableData.push([oldLang('Stars.Transaction.Reaction.Post'), <SafeLink url={messageLink} text={messageLink} />]);
|
||||
}
|
||||
@ -252,7 +280,7 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
tableData,
|
||||
footer,
|
||||
};
|
||||
}, [transaction, oldLang, lang, peer, canPlayAnimatedEmojis, topSticker]);
|
||||
}, [transaction, oldLang, lang, peer, canPlayAnimatedEmojis, topSticker, paidMessageCommission]);
|
||||
|
||||
const prevModalData = usePrevious(starModalData);
|
||||
const renderingModalData = prevModalData || starModalData;
|
||||
@ -275,6 +303,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
(global, { modal }): StateProps => {
|
||||
const peerId = modal?.transaction?.peer?.type === 'peer' && modal.transaction.peer.id;
|
||||
const peer = peerId ? selectPeer(global, peerId) : undefined;
|
||||
const paidMessageCommission = global.appConfig?.starsPaidMessageCommissionPermille;
|
||||
|
||||
const starCount = modal?.transaction.stars;
|
||||
const starsGiftSticker = modal?.transaction.isGift && selectGiftStickerForStars(global, starCount?.amount);
|
||||
@ -283,6 +312,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
peer,
|
||||
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
|
||||
topSticker: starsGiftSticker,
|
||||
paidMessageCommission,
|
||||
};
|
||||
},
|
||||
)(StarsTransactionModal));
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
selectChat,
|
||||
selectIsCurrentUserPremium,
|
||||
selectPeer,
|
||||
selectPeerPaidMessagesStars,
|
||||
selectPeerStory,
|
||||
selectPerformanceSettingsValue,
|
||||
selectTabState,
|
||||
@ -30,6 +31,7 @@ import buildClassName from '../../util/buildClassName';
|
||||
import captureKeyboardListeners from '../../util/captureKeyboardListeners';
|
||||
import { formatMediaDuration, formatRelativePastTime } from '../../util/dates/dateFormat';
|
||||
import download from '../../util/download';
|
||||
import { formatStarsAsIcon } from '../../util/localization/format';
|
||||
import { round } from '../../util/math';
|
||||
import { getServerTime } from '../../util/serverTime';
|
||||
import { IS_SAFARI } from '../../util/windowEnvironment';
|
||||
@ -43,6 +45,7 @@ import useCanvasBlur from '../../hooks/useCanvasBlur';
|
||||
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useLongPress from '../../hooks/useLongPress';
|
||||
import useMediaTransitionDeprecated from '../../hooks/useMediaTransitionDeprecated';
|
||||
@ -99,6 +102,7 @@ interface StateProps {
|
||||
isCurrentUserPremium?: boolean;
|
||||
stealthMode: ApiStealthMode;
|
||||
withHeaderAnimation?: boolean;
|
||||
paidMessagesStars?: number;
|
||||
}
|
||||
|
||||
const VIDEO_MIN_READY_STATE = IS_SAFARI ? 4 : 3;
|
||||
@ -131,6 +135,7 @@ function Story({
|
||||
onDelete,
|
||||
onClose,
|
||||
onReport,
|
||||
paidMessagesStars,
|
||||
}: OwnProps & StateProps) {
|
||||
const {
|
||||
viewStory,
|
||||
@ -151,7 +156,8 @@ function Story({
|
||||
} = getActions();
|
||||
const serverTime = getServerTime();
|
||||
|
||||
const lang = useOldLang();
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
const { isMobile } = useAppLayout();
|
||||
const [isComposerHasFocus, markComposerHasFocus, unmarkComposerHasFocus] = useFlag(false);
|
||||
const [isStoryPlaybackRequested, playStory, pauseStory] = useFlag(false);
|
||||
@ -199,7 +205,7 @@ function Story({
|
||||
isOut && (story!.date + viewersExpirePeriod) < getServerTime(),
|
||||
);
|
||||
|
||||
const forwardSenderTitle = forwardSender ? getPeerTitle(lang, forwardSender)
|
||||
const forwardSenderTitle = forwardSender ? getPeerTitle(oldLang, forwardSender)
|
||||
: (isLoadedStory && story.forwardInfo?.fromName);
|
||||
|
||||
const canCopyLink = Boolean(
|
||||
@ -492,16 +498,16 @@ function Story({
|
||||
: story.isForContacts ? 'contacts' : (story.isForCloseFriends ? 'closeFriends' : 'nobody');
|
||||
|
||||
let message;
|
||||
const myName = getPeerTitle(lang, peer);
|
||||
const myName = getPeerTitle(oldLang, peer);
|
||||
switch (visibility) {
|
||||
case 'nobody':
|
||||
message = lang('StorySelectedContactsHint', myName);
|
||||
message = oldLang('StorySelectedContactsHint', myName);
|
||||
break;
|
||||
case 'contacts':
|
||||
message = lang('StoryContactsHint', myName);
|
||||
message = oldLang('StoryContactsHint', myName);
|
||||
break;
|
||||
case 'closeFriends':
|
||||
message = lang('StoryCloseFriendsHint', myName);
|
||||
message = oldLang('StoryCloseFriendsHint', myName);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
@ -512,7 +518,7 @@ function Story({
|
||||
const handleVolumeMuted = useLastCallback(() => {
|
||||
if (noSound) {
|
||||
showNotification({
|
||||
message: lang('Story.TooltipVideoHasNoSound'),
|
||||
message: oldLang('Story.TooltipVideoHasNoSound'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -525,8 +531,8 @@ function Story({
|
||||
if (stealthMode.activeUntil && getServerTime() < stealthMode.activeUntil) {
|
||||
const diff = stealthMode.activeUntil - getServerTime();
|
||||
showNotification({
|
||||
title: lang('StealthModeOn'),
|
||||
message: lang('Story.ToastStealthModeActiveText', formatMediaDuration(diff)),
|
||||
title: oldLang('StealthModeOn'),
|
||||
message: oldLang('Story.ToastStealthModeActiveText', formatMediaDuration(diff)),
|
||||
duration: STEALTH_MODE_NOTIFICATION_DURATION,
|
||||
});
|
||||
return;
|
||||
@ -544,9 +550,9 @@ function Story({
|
||||
if (!isDeletedStory) return;
|
||||
|
||||
showNotification({
|
||||
message: lang('StoryNotFound'),
|
||||
message: oldLang('StoryNotFound'),
|
||||
});
|
||||
}, [lang, isDeletedStory]);
|
||||
}, [oldLang, isDeletedStory]);
|
||||
|
||||
const MenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
||||
return ({ onTrigger, isOpen }) => {
|
||||
@ -558,13 +564,13 @@ function Story({
|
||||
color="translucent-white"
|
||||
onClick={onTrigger}
|
||||
className={buildClassName(styles.button, isOpen && 'active')}
|
||||
ariaLabel={lang('AccDescrOpenMenu2')}
|
||||
ariaLabel={oldLang('AccDescrOpenMenu2')}
|
||||
>
|
||||
<Icon name="more" />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
}, [isMobile, lang]);
|
||||
}, [isMobile, oldLang]);
|
||||
|
||||
function renderStoriesTabs() {
|
||||
return (
|
||||
@ -643,7 +649,7 @@ function Story({
|
||||
/>
|
||||
<div className={styles.senderMeta}>
|
||||
<span onClick={handleOpenChat} className={styles.senderName}>
|
||||
{renderText(getPeerTitle(lang, peer) || '')}
|
||||
{renderText(getPeerTitle(oldLang, peer) || '')}
|
||||
</span>
|
||||
<div className={styles.storyMetaRow}>
|
||||
{forwardSenderTitle && (
|
||||
@ -668,15 +674,15 @@ function Story({
|
||||
>
|
||||
<Avatar peer={fromPeer} size="micro" />
|
||||
<span className={styles.headerTitle}>
|
||||
{renderText(getPeerTitle(lang, fromPeer) || '')}
|
||||
{renderText(getPeerTitle(oldLang, fromPeer) || '')}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
{story && 'date' in story && (
|
||||
<span className={styles.storyMeta}>{formatRelativePastTime(lang, serverTime, story.date)}</span>
|
||||
<span className={styles.storyMeta}>{formatRelativePastTime(oldLang, serverTime, story.date)}</span>
|
||||
)}
|
||||
{isLoadedStory && story.isEdited && (
|
||||
<span className={styles.storyMeta}>{lang('Story.HeaderEdited')}</span>
|
||||
<span className={styles.storyMeta}>{oldLang('Story.HeaderEdited')}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -702,7 +708,7 @@ function Story({
|
||||
color="translucent-white"
|
||||
disabled={!hasFullData}
|
||||
onClick={handleVolumeMuted}
|
||||
ariaLabel={lang('Volume')}
|
||||
ariaLabel={oldLang('Volume')}
|
||||
>
|
||||
<Icon name={(isMuted || noSound) ? 'speaker-muted-story' : 'speaker-story'} />
|
||||
</Button>
|
||||
@ -714,36 +720,43 @@ function Story({
|
||||
onOpen={handleDropdownMenuOpen}
|
||||
onClose={handleDropdownMenuClose}
|
||||
>
|
||||
{canCopyLink && <MenuItem icon="copy" onClick={handleCopyStoryLink}>{lang('CopyLink')}</MenuItem>}
|
||||
{canCopyLink && <MenuItem icon="copy" onClick={handleCopyStoryLink}>{oldLang('CopyLink')}</MenuItem>}
|
||||
{canPinToProfile && (
|
||||
<MenuItem icon="save-story" onClick={handlePinClick}>
|
||||
{lang(isUserStory ? 'StorySave' : 'SaveToPosts')}
|
||||
{oldLang(isUserStory ? 'StorySave' : 'SaveToPosts')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{canUnpinFromProfile && (
|
||||
<MenuItem icon="delete" onClick={handleUnpinClick}>
|
||||
{lang(isUserStory ? 'ArchiveStory' : 'RemoveFromPosts')}
|
||||
{oldLang(isUserStory ? 'ArchiveStory' : 'RemoveFromPosts')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{canDownload && (
|
||||
<MenuItem icon="download" disabled={!downloadMediaData} onClick={handleDownload}>
|
||||
{lang('lng_media_download')}
|
||||
{oldLang('lng_media_download')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!isOut && isUserStory && (
|
||||
<MenuItem icon="eye-crossed-outline" onClick={handleOpenStealthModal}>
|
||||
{lang('StealthMode')}
|
||||
{oldLang('StealthMode')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!isOut && <MenuItem icon="flag" onClick={handleReportStoryClick}>{oldLang('lng_report_story')}</MenuItem>}
|
||||
{isOut && (
|
||||
<MenuItem
|
||||
icon="delete"
|
||||
destructive
|
||||
onClick={handleDeleteStoryClick}
|
||||
>{oldLang('Delete')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!isOut && <MenuItem icon="flag" onClick={handleReportStoryClick}>{lang('lng_report_story')}</MenuItem>}
|
||||
{isOut && <MenuItem icon="delete" destructive onClick={handleDeleteStoryClick}>{lang('Delete')}</MenuItem>}
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
className={buildClassName(styles.button, styles.closeButton)}
|
||||
round
|
||||
size="tiny"
|
||||
color="translucent-white"
|
||||
ariaLabel={lang('Close')}
|
||||
ariaLabel={oldLang('Close')}
|
||||
onClick={onClose}
|
||||
>
|
||||
<Icon name="close" />
|
||||
@ -753,6 +766,14 @@ function Story({
|
||||
);
|
||||
}
|
||||
|
||||
const inputPlaceholder = paidMessagesStars
|
||||
? lang('ComposerPlaceholderPaidReply', {
|
||||
amount: formatStarsAsIcon(lang, paidMessagesStars, { asFont: true, className: 'placeholder-star-icon' }),
|
||||
}, {
|
||||
withNodes: true,
|
||||
})
|
||||
: oldLang(isChatStory ? 'ReplyToGroupStory' : 'ReplyPrivately');
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName(styles.slideInner, 'component-theme-dark')}
|
||||
@ -821,13 +842,13 @@ function Story({
|
||||
type="button"
|
||||
className={buildClassName(styles.navigate, styles.prev)}
|
||||
onClick={handleOpenPrevStory}
|
||||
aria-label={lang('Previous')}
|
||||
aria-label={oldLang('Previous')}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={buildClassName(styles.navigate, styles.next)}
|
||||
onClick={handleOpenNextStory}
|
||||
aria-label={lang('Next')}
|
||||
aria-label={oldLang('Next')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@ -847,7 +868,7 @@ function Story({
|
||||
withStory
|
||||
storyViewerMode="disabled"
|
||||
/>
|
||||
<div className={styles.name}>{renderText(getPeerTitle(lang, peer) || '')}</div>
|
||||
<div className={styles.name}>{renderText(getPeerTitle(oldLang, peer) || '')}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -862,7 +883,7 @@ function Story({
|
||||
role="button"
|
||||
className={buildClassName(styles.captionBackdrop, captionBackdropTransitionClassNames)}
|
||||
onClick={() => foldCaption()}
|
||||
aria-label={lang('Close')}
|
||||
aria-label={oldLang('Close')}
|
||||
/>
|
||||
)}
|
||||
{hasText && <div className={buildClassName(styles.captionGradient, captionAppearanceAnimationClassNames)} />}
|
||||
@ -889,7 +910,7 @@ function Story({
|
||||
editableInputId={EDITABLE_STORY_INPUT_ID}
|
||||
inputId="story-input-text"
|
||||
className={buildClassName(styles.composer, composerAppearanceAnimationClassNames)}
|
||||
inputPlaceholder={lang(isChatStory ? 'ReplyToGroupStory' : 'ReplyPrivately')}
|
||||
inputPlaceholder={inputPlaceholder}
|
||||
onForward={canShare ? handleForwardClick : undefined}
|
||||
onFocus={markComposerHasFocus}
|
||||
onBlur={unmarkComposerHasFocus}
|
||||
@ -923,12 +944,14 @@ export default memo(withGlobal<OwnProps>((global, {
|
||||
mapModal,
|
||||
reportModal,
|
||||
giftInfoModal,
|
||||
isPaymentMessageConfirmDialogOpen,
|
||||
} = tabState;
|
||||
const { isOpen: isPremiumModalOpen } = premiumModal || {};
|
||||
const story = selectPeerStory(global, peerId, storyId);
|
||||
const isLoadedStory = story && 'content' in story;
|
||||
const shouldForcePause = Boolean(
|
||||
viewModal || forwardedStoryId || tabState.reactionPicker?.storyId || reportModal || isPrivacyModalOpen
|
||||
isPaymentMessageConfirmDialogOpen
|
||||
|| viewModal || forwardedStoryId || tabState.reactionPicker?.storyId || reportModal || isPrivacyModalOpen
|
||||
|| isPremiumModalOpen || isDeleteModalOpen || safeLinkModalUrl || isStealthModalOpen || mapModal || giftInfoModal,
|
||||
);
|
||||
|
||||
@ -940,6 +963,7 @@ export default memo(withGlobal<OwnProps>((global, {
|
||||
const withHeaderAnimation = selectPerformanceSettingsValue(global, 'mediaViewerAnimations');
|
||||
|
||||
const fromPeer = isLoadedStory && story.fromId ? selectPeer(global, story.fromId) : undefined;
|
||||
const paidMessagesStars = selectPeerPaidMessagesStars(global, peerId);
|
||||
|
||||
return {
|
||||
peer: (user || chat)!,
|
||||
@ -956,5 +980,6 @@ export default memo(withGlobal<OwnProps>((global, {
|
||||
arePeerSettingsLoaded: Boolean(userFullInfo?.settings),
|
||||
stealthMode: global.stories.stealthMode,
|
||||
withHeaderAnimation,
|
||||
paidMessagesStars,
|
||||
};
|
||||
})(Story));
|
||||
|
||||
@ -60,7 +60,7 @@
|
||||
}
|
||||
|
||||
.notification-button {
|
||||
color: var(--color-primary);
|
||||
color: var(--color-toast-action);
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-transform: none;
|
||||
margin-inline-start: 0.125rem;
|
||||
|
||||
@ -22,6 +22,7 @@ import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated
|
||||
|
||||
import CustomEmoji from '../common/CustomEmoji';
|
||||
import Icon from '../common/icons/Icon';
|
||||
import StarIcon from '../common/icons/StarIcon';
|
||||
import Button from './Button';
|
||||
import Portal from './Portal';
|
||||
import RoundTimer from './RoundTimer';
|
||||
@ -54,6 +55,7 @@ const Notification: FC<OwnProps> = ({
|
||||
dismissAction,
|
||||
duration = DEFAULT_DURATION,
|
||||
icon,
|
||||
shouldUseCustomIcon,
|
||||
customEmojiIconId,
|
||||
shouldShowTimer,
|
||||
title,
|
||||
@ -79,6 +81,23 @@ const Notification: FC<OwnProps> = ({
|
||||
}
|
||||
});
|
||||
|
||||
const handleActionClick = useLastCallback(() => {
|
||||
if (action) {
|
||||
if (Array.isArray(action)) {
|
||||
// @ts-ignore
|
||||
action.forEach((cb) => actions[cb.action](cb.payload));
|
||||
} else {
|
||||
// @ts-ignore
|
||||
actions[action.action](action.payload);
|
||||
}
|
||||
}
|
||||
if (disableClickDismiss) {
|
||||
setIsOpen(false);
|
||||
setTimeout(handleDismiss, ANIMATION_DURATION + ANIMATION_END_DELAY);
|
||||
}
|
||||
closeAndDismiss();
|
||||
});
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
if (action) {
|
||||
if (Array.isArray(action)) {
|
||||
@ -151,6 +170,28 @@ const Notification: FC<OwnProps> = ({
|
||||
return actionText;
|
||||
}, [lang, actionText]);
|
||||
|
||||
const renderedIcon = useMemo(() => {
|
||||
if (customEmojiIconId) {
|
||||
return (
|
||||
<CustomEmoji
|
||||
className="notification-emoji-icon"
|
||||
forceAlways
|
||||
size={CUSTOM_EMOJI_SIZE}
|
||||
documentId={customEmojiIconId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (shouldUseCustomIcon) {
|
||||
if (icon === 'star') {
|
||||
return (
|
||||
<StarIcon type="gold" className={buildClassName('notification-icon')} size="adaptive" />
|
||||
);
|
||||
}
|
||||
}
|
||||
return <Icon name={icon || 'info-filled'} className="notification-icon" />;
|
||||
}, [customEmojiIconId, icon, shouldUseCustomIcon]);
|
||||
|
||||
return (
|
||||
<Portal className="Notification-container" containerSelector={containerSelector}>
|
||||
<div
|
||||
@ -159,26 +200,17 @@ const Notification: FC<OwnProps> = ({
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{customEmojiIconId ? (
|
||||
<CustomEmoji
|
||||
className="notification-emoji-icon"
|
||||
forceAlways
|
||||
size={CUSTOM_EMOJI_SIZE}
|
||||
documentId={customEmojiIconId}
|
||||
/>
|
||||
) : (
|
||||
<Icon name={icon || 'info-filled'} className="notification-icon" />
|
||||
)}
|
||||
{renderedIcon}
|
||||
<div className="content">
|
||||
{renderedTitle && (
|
||||
<div className="notification-title">{renderedTitle}</div>
|
||||
)}
|
||||
{renderedMessage}
|
||||
</div>
|
||||
{action && renderedActionText && (
|
||||
{renderedActionText && (
|
||||
<Button
|
||||
color="translucent-white"
|
||||
onClick={handleClick}
|
||||
onClick={handleActionClick}
|
||||
className="notification-button"
|
||||
>
|
||||
{renderedActionText}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
.root {
|
||||
position: relative;
|
||||
color: var(--color-primary);
|
||||
color: var(--color-toast-action);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
}
|
||||
|
||||
.circle {
|
||||
stroke: var(--color-primary);
|
||||
stroke: var(--color-toast-action);
|
||||
fill: transparent;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
|
||||
@ -19,6 +19,7 @@ export const IS_TEST = process.env.APP_ENV === 'test';
|
||||
export const IS_PERF = process.env.APP_ENV === 'perf';
|
||||
export const IS_BETA = process.env.APP_ENV === 'staging';
|
||||
export const IS_PACKAGED_ELECTRON = process.env.IS_PACKAGED_ELECTRON;
|
||||
export const PAID_MESSAGES_PURPOSE = 'paid_messages';
|
||||
|
||||
export const DEBUG = process.env.APP_ENV !== 'production';
|
||||
export const DEBUG_MORE = false;
|
||||
@ -121,6 +122,10 @@ export const TOP_CHAT_MESSAGES_PRELOAD_LIMIT = 20;
|
||||
|
||||
export const SPONSORED_MESSAGE_CACHE_MS = 300000; // 5 min
|
||||
|
||||
export const DEFAULT_CHARGE_FOR_MESSAGES = 250;
|
||||
export const MINIMUM_CHARGE_FOR_MESSAGES = 1;
|
||||
export const DEFAULT_MAXIMUM_CHARGE_FOR_MESSAGES = 10000;
|
||||
|
||||
export const DEFAULT_VOLUME = 1;
|
||||
export const DEFAULT_PLAYBACK_RATE = 1;
|
||||
export const PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION = 20 * 60; // 20 min
|
||||
@ -177,6 +182,7 @@ export const TMP_CHAT_ID = '0';
|
||||
export const ANIMATION_END_DELAY = 100;
|
||||
export const ANIMATION_WAVE_MIN_INTERVAL = 200;
|
||||
export const MESSAGE_APPEARANCE_DELAY = 10;
|
||||
export const PAID_SEND_DELAY = 5000;
|
||||
|
||||
export const SCROLL_MIN_DURATION = 300;
|
||||
export const SCROLL_MAX_DURATION = 600;
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
} from '../../../api/types';
|
||||
import { ManagementProgress } from '../../../types';
|
||||
|
||||
import { BOT_FATHER_USERNAME, GENERAL_REFETCH_INTERVAL } from '../../../config';
|
||||
import { BOT_FATHER_USERNAME, GENERAL_REFETCH_INTERVAL, PAID_SEND_DELAY } from '../../../config';
|
||||
import { copyTextToClipboard } from '../../../util/clipboard';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { oldTranslate } from '../../../util/oldLangProvider';
|
||||
@ -62,6 +62,7 @@ import {
|
||||
selectUserFullInfo,
|
||||
} from '../../selectors';
|
||||
import { fetchChatByUsername } from './chats';
|
||||
import { getPeerStarsForMessage } from './messages';
|
||||
|
||||
import { getIsWebAppsFullscreenSupported } from '../../../hooks/useAppLayout';
|
||||
|
||||
@ -379,7 +380,26 @@ addActionHandler('switchBotInline', (global, actions, payload): ActionReturnType
|
||||
return undefined;
|
||||
});
|
||||
|
||||
addActionHandler('sendInlineBotResult', (global, actions, payload): ActionReturnType => {
|
||||
addActionHandler('sendInlineBotApiResult', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
chat, id, queryId, replyInfo, sendAs, isSilent, scheduledAt, allowPaidStars,
|
||||
} = payload;
|
||||
|
||||
await callApi('sendInlineBotResult', {
|
||||
chat,
|
||||
resultId: id,
|
||||
queryId,
|
||||
replyInfo,
|
||||
sendAs,
|
||||
isSilent,
|
||||
scheduleDate: scheduledAt,
|
||||
allowPaidStars,
|
||||
});
|
||||
|
||||
if (allowPaidStars) actions.loadStarStatus();
|
||||
});
|
||||
|
||||
addActionHandler('sendInlineBotResult', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
id, queryId, isSilent, scheduledAt, threadId, chatId,
|
||||
tabId = getCurrentTabId(),
|
||||
@ -396,14 +416,39 @@ addActionHandler('sendInlineBotResult', (global, actions, payload): ActionReturn
|
||||
actions.resetDraftReplyInfo({ tabId });
|
||||
actions.clearWebPagePreview({ tabId });
|
||||
|
||||
void callApi('sendInlineBotResult', {
|
||||
const starsForOneMessage = await getPeerStarsForMessage(global, chat);
|
||||
const params = {
|
||||
chat,
|
||||
resultId: id,
|
||||
id,
|
||||
queryId,
|
||||
replyInfo,
|
||||
sendAs: selectSendAs(global, chatId),
|
||||
isSilent,
|
||||
scheduleDate: scheduledAt,
|
||||
scheduledAt,
|
||||
allowPaidStars: starsForOneMessage,
|
||||
};
|
||||
if (!starsForOneMessage) {
|
||||
actions.sendInlineBotApiResult(params);
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line eslint-multitab-tt/no-getactions-in-actions
|
||||
actions.showNotification({
|
||||
localId: queryId,
|
||||
title: { key: 'ToastTitleMessageSent' },
|
||||
message: { key: 'ToastMessageSent', variables: { amount: starsForOneMessage } },
|
||||
actionText: { key: 'ButtonUndo' },
|
||||
dismissAction: {
|
||||
action: 'sendInlineBotApiResult',
|
||||
payload: params,
|
||||
},
|
||||
duration: PAID_SEND_DELAY,
|
||||
shouldShowTimer: true,
|
||||
disableClickDismiss: true,
|
||||
icon: 'star',
|
||||
shouldUseCustomIcon: true,
|
||||
type: 'paidMessage',
|
||||
tabId,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -5,33 +5,33 @@ import type {
|
||||
ApiDraft,
|
||||
ApiError,
|
||||
ApiInputMessageReplyInfo,
|
||||
ApiInputReplyInfo,
|
||||
ApiInputStoryReplyInfo,
|
||||
ApiMessage,
|
||||
ApiMessageEntity,
|
||||
ApiNewPoll,
|
||||
ApiOnProgress,
|
||||
ApiPeer,
|
||||
ApiSticker,
|
||||
ApiStory,
|
||||
ApiStorySkipped,
|
||||
ApiUser,
|
||||
ApiVideo,
|
||||
} from '../../../api/types';
|
||||
import type {
|
||||
ForwardMessagesParams,
|
||||
SendMessageParams,
|
||||
ThreadId,
|
||||
} from '../../../types';
|
||||
import type { MessageKey } from '../../../util/keys/messageKey';
|
||||
import type { RegularLangFnParameters } from '../../../util/localization';
|
||||
import type { RequiredGlobalActions } from '../../index';
|
||||
import type {
|
||||
ActionReturnType, GlobalState, TabArgs,
|
||||
} from '../../types';
|
||||
import { MAIN_THREAD_ID, MESSAGE_DELETED } from '../../../api/types';
|
||||
import { LoadMoreDirection, type ThreadId, type WebPageMediaSize } from '../../../types';
|
||||
import { LoadMoreDirection } from '../../../types';
|
||||
|
||||
import {
|
||||
GIF_MIME_TYPE,
|
||||
MAX_MEDIA_FILES_FOR_ALBUM,
|
||||
MESSAGE_ID_REQUIRED_ERROR,
|
||||
MESSAGE_LIST_SLICE,
|
||||
RE_TELEGRAM_LINK,
|
||||
PAID_SEND_DELAY, RE_TELEGRAM_LINK,
|
||||
SERVICE_NOTIFICATIONS_USER_ID,
|
||||
SUPPORTED_AUDIO_CONTENT_TYPES,
|
||||
SUPPORTED_PHOTO_CONTENT_TYPES,
|
||||
@ -61,6 +61,7 @@ import {
|
||||
isChatSuperGroup,
|
||||
isDeletedUser,
|
||||
isMessageLocal,
|
||||
isPeerUser,
|
||||
isServiceNotificationMessage,
|
||||
isUserBot,
|
||||
splitMessagesForForwarding,
|
||||
@ -301,14 +302,14 @@ addActionHandler('loadMessage', async (global, actions, payload): Promise<void>
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('sendMessage', (global, actions, payload): ActionReturnType => {
|
||||
addActionHandler('sendMessage', async (global, actions, payload): Promise<void> => {
|
||||
const { messageList, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
const { storyId, peerId: storyPeerId } = selectCurrentViewedStory(global, tabId);
|
||||
const isStoryReply = Boolean(storyId && storyPeerId);
|
||||
|
||||
if (!messageList && !isStoryReply) {
|
||||
return undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
let { chatId, threadId, type } = messageList || {};
|
||||
@ -321,9 +322,11 @@ addActionHandler('sendMessage', (global, actions, payload): ActionReturnType =>
|
||||
payload = omit(payload, ['tabId']);
|
||||
|
||||
if (type === 'scheduled' && !payload.scheduledAt) {
|
||||
return updateTabState(global, {
|
||||
global = updateTabState(global, {
|
||||
contentToBeScheduled: payload,
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
return;
|
||||
}
|
||||
|
||||
const chat = selectChat(global, chatId!)!;
|
||||
@ -342,30 +345,36 @@ addActionHandler('sendMessage', (global, actions, payload): ActionReturnType =>
|
||||
|
||||
const replyInfo = storyReplyInfo || messageReplyInfo;
|
||||
const lastMessageId = selectChatLastMessageId(global, chatId!);
|
||||
const messagePriceInStars = await getPeerStarsForMessage(global, chat);
|
||||
|
||||
const params = {
|
||||
const params : SendMessageParams = {
|
||||
...payload,
|
||||
chat,
|
||||
replyInfo,
|
||||
noWebPage: selectNoWebPage(global, chatId!, threadId!),
|
||||
sendAs: selectSendAs(global, chatId!),
|
||||
lastMessageId,
|
||||
messagePriceInStars,
|
||||
isStoryReply,
|
||||
isPending: messagePriceInStars ? true : undefined,
|
||||
};
|
||||
|
||||
if (!isStoryReply) {
|
||||
actions.clearWebPagePreview({ tabId });
|
||||
}
|
||||
|
||||
const isSingle = !payload.attachments || payload.attachments.length <= 1;
|
||||
const isSingle = (!payload.attachments || payload.attachments.length <= 1) && !isForwarding;
|
||||
const isGrouped = !isSingle && payload.shouldGroupMessages;
|
||||
const localMessages: SendMessageParams[] = [];
|
||||
|
||||
if (isSingle) {
|
||||
const { attachments, ...restParams } = params;
|
||||
sendMessage(global, {
|
||||
const sendParams: SendMessageParams = {
|
||||
...restParams,
|
||||
attachment: attachments ? attachments[0] : undefined,
|
||||
wasDrafted: Boolean(draft),
|
||||
});
|
||||
};
|
||||
await sendMessageOrReduceLocal(global, sendParams, localMessages);
|
||||
} else if (isGrouped) {
|
||||
const {
|
||||
text, entities, attachments, ...commonParams
|
||||
@ -373,7 +382,8 @@ addActionHandler('sendMessage', (global, actions, payload): ActionReturnType =>
|
||||
const byType = splitAttachmentsByType(attachments!);
|
||||
|
||||
let hasSentCaption = false;
|
||||
byType.forEach((group, groupIndex) => {
|
||||
for (let groupIndex = 0; groupIndex < byType.length; groupIndex++) {
|
||||
const group = byType[groupIndex];
|
||||
const groupedAttachments = split(group as ApiAttachment[], MAX_MEDIA_FILES_FOR_ALBUM);
|
||||
for (let i = 0; i < groupedAttachments.length; i++) {
|
||||
const groupedId = `${Date.now()}${groupIndex}${i}`;
|
||||
@ -383,70 +393,86 @@ addActionHandler('sendMessage', (global, actions, payload): ActionReturnType =>
|
||||
|
||||
if (group[0].quick && !group[0].shouldSendAsFile) {
|
||||
const [firstAttachment, ...restAttachments] = groupedAttachments[i];
|
||||
sendMessage(global, {
|
||||
|
||||
let sendParams: SendMessageParams = {
|
||||
...commonParams,
|
||||
text: isFirst && !hasSentCaption ? text : undefined,
|
||||
entities: isFirst && !hasSentCaption ? entities : undefined,
|
||||
attachment: firstAttachment,
|
||||
groupedId: restAttachments.length > 0 ? groupedId : undefined,
|
||||
wasDrafted: Boolean(draft),
|
||||
});
|
||||
};
|
||||
await sendMessageOrReduceLocal(global, sendParams, localMessages);
|
||||
|
||||
hasSentCaption = true;
|
||||
|
||||
restAttachments.forEach((attachment: ApiAttachment) => {
|
||||
sendMessage(global, {
|
||||
for (const attachment of restAttachments) {
|
||||
sendParams = {
|
||||
...commonParams,
|
||||
attachment,
|
||||
groupedId,
|
||||
});
|
||||
});
|
||||
};
|
||||
await sendMessageOrReduceLocal(global, sendParams, localMessages);
|
||||
}
|
||||
} else {
|
||||
const firstAttachments = groupedAttachments[i].slice(0, -1);
|
||||
const lastAttachment = groupedAttachments[i][groupedAttachments[i].length - 1];
|
||||
firstAttachments.forEach((attachment: ApiAttachment) => {
|
||||
sendMessage(global, {
|
||||
for (const attachment of firstAttachments) {
|
||||
const sendParams = {
|
||||
...commonParams,
|
||||
attachment,
|
||||
groupedId,
|
||||
});
|
||||
});
|
||||
};
|
||||
await sendMessageOrReduceLocal(global, sendParams, localMessages);
|
||||
}
|
||||
|
||||
sendMessage(global, {
|
||||
const sendParams = {
|
||||
...commonParams,
|
||||
text: isLast && !hasSentCaption ? text : undefined,
|
||||
entities: isLast && !hasSentCaption ? entities : undefined,
|
||||
attachment: lastAttachment,
|
||||
groupedId: firstAttachments.length > 0 ? groupedId : undefined,
|
||||
wasDrafted: Boolean(draft),
|
||||
});
|
||||
};
|
||||
await sendMessageOrReduceLocal(global, sendParams, localMessages);
|
||||
|
||||
hasSentCaption = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const {
|
||||
text, entities, attachments, replyInfo: replyToForFirstMessage, ...commonParams
|
||||
} = params;
|
||||
|
||||
if (text) {
|
||||
sendMessage(global, {
|
||||
const sendParams = {
|
||||
...commonParams,
|
||||
text,
|
||||
entities,
|
||||
replyInfo: replyToForFirstMessage,
|
||||
wasDrafted: Boolean(draft),
|
||||
});
|
||||
};
|
||||
await sendMessageOrReduceLocal(global, sendParams, localMessages);
|
||||
}
|
||||
|
||||
attachments?.forEach((attachment: ApiAttachment) => {
|
||||
sendMessage(global, {
|
||||
...commonParams,
|
||||
attachment,
|
||||
});
|
||||
});
|
||||
if (attachments) {
|
||||
for (const attachment of attachments) {
|
||||
const sendParams = {
|
||||
...commonParams,
|
||||
attachment,
|
||||
};
|
||||
await sendMessageOrReduceLocal(global, sendParams, localMessages);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
if (isForwarding) {
|
||||
const localForwards = await executeForwardMessages(global, params, tabId);
|
||||
if (localForwards) {
|
||||
localMessages.push(...localForwards);
|
||||
}
|
||||
}
|
||||
if (localMessages?.length) sendMessagesWithNotification(global, localMessages);
|
||||
});
|
||||
|
||||
addActionHandler('sendInviteMessages', async (global, actions, payload): Promise<void> => {
|
||||
@ -735,21 +761,37 @@ addActionHandler('unpinAllMessages', async (global, actions, payload): Promise<v
|
||||
});
|
||||
|
||||
addActionHandler('deleteMessages', (global, actions, payload): ActionReturnType => {
|
||||
const { messageIds, shouldDeleteForAll, tabId = getCurrentTabId() } = payload!;
|
||||
const {
|
||||
messageIds, shouldDeleteForAll, messageList: payloadMessageList, tabId = getCurrentTabId(),
|
||||
} = payload!;
|
||||
const currentMessageList = selectCurrentMessageList(global, tabId);
|
||||
if (!currentMessageList) {
|
||||
const messageList = payloadMessageList || currentMessageList;
|
||||
if (!messageList) {
|
||||
return;
|
||||
}
|
||||
const { chatId, threadId } = currentMessageList;
|
||||
const { chatId, threadId } = messageList;
|
||||
const chat = selectChat(global, chatId)!;
|
||||
const messageIdsToDelete = messageIds.filter((id) => {
|
||||
const message = selectChatMessage(global, chatId, id);
|
||||
return message && !isMessageLocal(message);
|
||||
});
|
||||
|
||||
Object.values(global.byTabId)
|
||||
.forEach(({ id }) => {
|
||||
messageIds.forEach((messageId) => {
|
||||
const message = selectChatMessage(global, chatId, messageId);
|
||||
if (message) {
|
||||
actions.dismissNotification({
|
||||
localId: getMessageKey(message),
|
||||
tabId: id,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Only local messages
|
||||
if (!messageIdsToDelete.length && messageIds.length) {
|
||||
deleteMessages(global, isChatChannel(chat) ? chatId : undefined, messageIds, actions);
|
||||
deleteMessages(global, isChatChannel(chat) || isChatSuperGroup(chat) ? chatId : undefined, messageIds, actions);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -761,6 +803,25 @@ addActionHandler('deleteMessages', (global, actions, payload): ActionReturnType
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('resetLocalPaidMessages', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
const notifications = selectTabState(global, tabId).notifications;
|
||||
if (!notifications || !notifications.length) return global;
|
||||
|
||||
notifications.forEach((notification) => {
|
||||
if (notification.type === 'paidMessage') {
|
||||
const action = notification.dismissAction;
|
||||
if (action && !Array.isArray(action)) {
|
||||
// @ts-ignore
|
||||
actions[action.action](action.payload);
|
||||
}
|
||||
actions.dismissNotification({ localId: notification.localId, tabId });
|
||||
}
|
||||
});
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('deleteParticipantHistory', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
chatId, peerId,
|
||||
@ -1129,91 +1190,6 @@ addActionHandler('loadExtendedMedia', (global, actions, payload): ActionReturnTy
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('forwardMessages', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
isSilent, scheduledAt, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
const {
|
||||
fromChatId, messageIds, toChatId, withMyScore, noAuthors, noCaptions, toThreadId = MAIN_THREAD_ID,
|
||||
} = selectTabState(global, tabId).forwardMessages;
|
||||
|
||||
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
||||
const isToMainThread = toThreadId === MAIN_THREAD_ID;
|
||||
|
||||
const fromChat = fromChatId ? selectChat(global, fromChatId) : undefined;
|
||||
const toChat = toChatId ? selectChat(global, toChatId) : undefined;
|
||||
|
||||
const messages = fromChatId && messageIds
|
||||
? messageIds
|
||||
.sort((a, b) => a - b)
|
||||
.map((id) => selectChatMessage(global, fromChatId, id)).filter(Boolean)
|
||||
: undefined;
|
||||
|
||||
if (!fromChat || !toChat || !messages || (toThreadId && !isToMainThread && !toChat.isForum)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sendAs = selectSendAs(global, toChatId!);
|
||||
const draft = selectDraft(global, toChatId!, toThreadId || MAIN_THREAD_ID);
|
||||
const lastMessageId = selectChatLastMessageId(global, toChat.id);
|
||||
|
||||
const [realMessages, serviceMessages] = partition(messages, (m) => !isServiceNotificationMessage(m));
|
||||
const forwardableRealMessages = realMessages.filter((message) => selectCanForwardMessage(global, message));
|
||||
if (forwardableRealMessages.length) {
|
||||
const messageBatches = global.config?.maxForwardedCount
|
||||
? splitMessagesForForwarding(forwardableRealMessages, global.config.maxForwardedCount)
|
||||
: [forwardableRealMessages];
|
||||
(async () => {
|
||||
await rafPromise(); // Wait one frame for any previous `sendMessage` to be processed
|
||||
messageBatches.forEach((batch) => {
|
||||
callApi('forwardMessages', {
|
||||
fromChat,
|
||||
toChat,
|
||||
toThreadId,
|
||||
messages: batch,
|
||||
isSilent,
|
||||
scheduledAt,
|
||||
sendAs,
|
||||
withMyScore,
|
||||
noAuthors,
|
||||
noCaptions,
|
||||
isCurrentUserPremium,
|
||||
wasDrafted: Boolean(draft),
|
||||
lastMessageId,
|
||||
});
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
serviceMessages
|
||||
.forEach((message) => {
|
||||
const { text, entities } = message.content.text || {};
|
||||
const { sticker } = message.content;
|
||||
|
||||
const replyInfo = selectMessageReplyInfo(global, toChat.id, toThreadId);
|
||||
|
||||
void sendMessage(global, {
|
||||
chat: toChat,
|
||||
replyInfo,
|
||||
text,
|
||||
entities,
|
||||
sticker,
|
||||
isSilent,
|
||||
scheduledAt,
|
||||
sendAs,
|
||||
lastMessageId,
|
||||
});
|
||||
});
|
||||
|
||||
global = getGlobal();
|
||||
global = updateTabState(global, {
|
||||
forwardMessages: {},
|
||||
isShareMessageModalShown: false,
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadScheduledHistory', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId } = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
@ -1336,6 +1312,110 @@ addActionHandler('loadCustomEmojis', async (global, actions, payload): Promise<v
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('forwardMessages', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
isSilent, scheduledAt, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
const { toChatId } = selectTabState(global, tabId).forwardMessages;
|
||||
const toChat = toChatId ? selectChat(global, toChatId) : undefined;
|
||||
if (!toChat) return;
|
||||
executeForwardMessages(global, { chat: toChat, isSilent, scheduledAt }, tabId);
|
||||
});
|
||||
|
||||
async function executeForwardMessages(global: GlobalState, sendParams: SendMessageParams, tabId: number) {
|
||||
const {
|
||||
fromChatId, messageIds, toChatId, withMyScore, noAuthors, noCaptions, toThreadId = MAIN_THREAD_ID,
|
||||
} = selectTabState(global, tabId).forwardMessages;
|
||||
const { messagePriceInStars, isSilent, scheduledAt } = sendParams;
|
||||
|
||||
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
||||
const isToMainThread = toThreadId === MAIN_THREAD_ID;
|
||||
|
||||
const fromChat = fromChatId ? selectChat(global, fromChatId) : undefined;
|
||||
const toChat = toChatId ? selectChat(global, toChatId) : undefined;
|
||||
|
||||
const messages = fromChatId && messageIds
|
||||
? messageIds
|
||||
.sort((a, b) => a - b)
|
||||
.map((id) => selectChatMessage(global, fromChatId, id)).filter(Boolean)
|
||||
: undefined;
|
||||
|
||||
if (!fromChat || !toChat || !messages || (toThreadId && !isToMainThread && !toChat.isForum)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sendAs = selectSendAs(global, toChatId!);
|
||||
const draft = selectDraft(global, toChatId!, toThreadId || MAIN_THREAD_ID);
|
||||
const lastMessageId = selectChatLastMessageId(global, toChat.id);
|
||||
const localMessages: SendMessageParams[] = [];
|
||||
|
||||
const [realMessages, serviceMessages] = partition(messages, (m) => !isServiceNotificationMessage(m));
|
||||
const forwardableRealMessages = realMessages.filter((message) => selectCanForwardMessage(global, message));
|
||||
if (forwardableRealMessages.length) {
|
||||
const messageSlices = global.config?.maxForwardedCount
|
||||
? splitMessagesForForwarding(forwardableRealMessages, global.config.maxForwardedCount)
|
||||
: [forwardableRealMessages];
|
||||
for (const slice of messageSlices) {
|
||||
const forwardParams: ForwardMessagesParams = {
|
||||
fromChat,
|
||||
toChat,
|
||||
toThreadId,
|
||||
messages: slice,
|
||||
isSilent,
|
||||
scheduledAt,
|
||||
sendAs,
|
||||
withMyScore,
|
||||
noAuthors,
|
||||
noCaptions,
|
||||
isCurrentUserPremium,
|
||||
wasDrafted: Boolean(draft),
|
||||
lastMessageId,
|
||||
messagePriceInStars,
|
||||
};
|
||||
|
||||
if (!messagePriceInStars) {
|
||||
callApi('forwardMessages', forwardParams);
|
||||
} else {
|
||||
const forwardedLocalMessagesSlice = await callApi('forwardMessagesLocal', forwardParams);
|
||||
localMessages.push({
|
||||
...sendParams,
|
||||
forwardParams: { ...forwardParams, forwardedLocalMessagesSlice },
|
||||
forwardedLocalMessagesSlice,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const message of serviceMessages) {
|
||||
const { text, entities } = message.content.text || {};
|
||||
const { sticker } = message.content;
|
||||
|
||||
const replyInfo = selectMessageReplyInfo(global, toChat.id, toThreadId);
|
||||
|
||||
const params: SendMessageParams = {
|
||||
chat: toChat,
|
||||
replyInfo,
|
||||
text,
|
||||
entities,
|
||||
sticker,
|
||||
isSilent,
|
||||
scheduledAt,
|
||||
sendAs,
|
||||
lastMessageId,
|
||||
};
|
||||
|
||||
await sendMessageOrReduceLocal(global, params, localMessages);
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
global = updateTabState(global, {
|
||||
forwardMessages: {},
|
||||
isShareMessageModalShown: false,
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
return localMessages;
|
||||
}
|
||||
|
||||
async function loadViewportMessages<T extends GlobalState>(
|
||||
global: T,
|
||||
chat: ApiChat,
|
||||
@ -1525,26 +1605,49 @@ function getViewportSlice(
|
||||
return { newViewportIds, areSomeLocal, areAllLocal };
|
||||
}
|
||||
|
||||
async function sendMessage<T extends GlobalState>(global: T, params: {
|
||||
chat: ApiChat;
|
||||
text?: string;
|
||||
entities?: ApiMessageEntity[];
|
||||
replyInfo?: ApiInputReplyInfo;
|
||||
attachment?: ApiAttachment;
|
||||
sticker?: ApiSticker;
|
||||
story?: ApiStory | ApiStorySkipped;
|
||||
gif?: ApiVideo;
|
||||
poll?: ApiNewPoll;
|
||||
isSilent?: boolean;
|
||||
scheduledAt?: number;
|
||||
sendAs?: ApiPeer;
|
||||
groupedId?: string;
|
||||
wasDrafted?: boolean;
|
||||
lastMessageId?: number;
|
||||
isInvertedMedia?: true;
|
||||
effectId?: string;
|
||||
webPageMediaSize?: WebPageMediaSize;
|
||||
}) {
|
||||
export async function getPeerStarsForMessage<T extends GlobalState>(
|
||||
global: T,
|
||||
peer: ApiPeer,
|
||||
): Promise<number | undefined> {
|
||||
if (!isPeerUser(peer)) {
|
||||
return peer.paidMessagesStars;
|
||||
}
|
||||
|
||||
if (!peer?.paidMessagesStars) return undefined;
|
||||
|
||||
const fullInfo = selectUserFullInfo(global, peer.id);
|
||||
if (fullInfo) {
|
||||
return fullInfo?.paidMessagesStars;
|
||||
}
|
||||
|
||||
const result = await callApi('fetchPaidMessagesStarsAmount', peer);
|
||||
return result;
|
||||
}
|
||||
|
||||
async function sendMessageOrReduceLocal<T extends GlobalState>(
|
||||
global: T,
|
||||
sendParams: SendMessageParams,
|
||||
localMessages: SendMessageParams[],
|
||||
) {
|
||||
if (!sendParams.messagePriceInStars) {
|
||||
sendMessage(global, sendParams);
|
||||
} else {
|
||||
const message = await callApi('sendMessageLocal', sendParams);
|
||||
if (message) {
|
||||
localMessages.push({
|
||||
...sendParams,
|
||||
localMessage: message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage<T extends GlobalState>(global: T, params: SendMessageParams) {
|
||||
// @optimization
|
||||
if (params.replyInfo || IS_IOS) {
|
||||
await rafPromise();
|
||||
}
|
||||
|
||||
let currentMessageKey: MessageKey | undefined;
|
||||
const progressCallback = params.attachment ? (progress: number, messageKey: MessageKey) => {
|
||||
if (!uploadProgressCallbacks.has(messageKey)) {
|
||||
@ -1556,14 +1659,7 @@ async function sendMessage<T extends GlobalState>(global: T, params: {
|
||||
global = updateUploadByMessageKey(global, messageKey, progress);
|
||||
setGlobal(global);
|
||||
} : undefined;
|
||||
|
||||
// @optimization
|
||||
if (params.replyInfo || IS_IOS) {
|
||||
await rafPromise();
|
||||
}
|
||||
|
||||
await callApi('sendMessage', params, progressCallback);
|
||||
|
||||
if (progressCallback && currentMessageKey) {
|
||||
global = getGlobal();
|
||||
global = updateUploadByMessageKey(global, currentMessageKey, undefined);
|
||||
@ -1573,6 +1669,92 @@ async function sendMessage<T extends GlobalState>(global: T, params: {
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessagesWithNotification<T extends GlobalState>(
|
||||
global: T,
|
||||
sendParams: SendMessageParams[],
|
||||
) {
|
||||
const chat = sendParams[0]?.chat;
|
||||
if (!chat || !sendParams.length) return;
|
||||
const starsForOneMessage = await getPeerStarsForMessage(global, chat);
|
||||
if (!starsForOneMessage) {
|
||||
// eslint-disable-next-line eslint-multitab-tt/no-getactions-in-actions
|
||||
getActions().sendMessages({ sendParams });
|
||||
return;
|
||||
}
|
||||
const messageIdsForUndo = sendParams.reduce((ids, params) => {
|
||||
if (params.localMessage?.id) {
|
||||
ids.push(params.localMessage.id);
|
||||
} else if (params.forwardedLocalMessagesSlice?.localMessages) {
|
||||
const forwardedIds = Object.values(params.forwardedLocalMessagesSlice.localMessages)
|
||||
.map((forwardedMessage) => forwardedMessage.id)
|
||||
.filter(Boolean);
|
||||
ids.push(...forwardedIds);
|
||||
}
|
||||
return ids;
|
||||
}, [] as number[]);
|
||||
|
||||
const localForwards = sendParams[0]?.forwardedLocalMessagesSlice?.localMessages;
|
||||
const firstMessage = sendParams[0]?.localMessage
|
||||
|| (localForwards && Object.values(localForwards)[0]);
|
||||
if (!firstMessage) return;
|
||||
|
||||
const messagesCount = messageIdsForUndo.length;
|
||||
|
||||
const firstSendParam = sendParams[0];
|
||||
let storySendMessage: RegularLangFnParameters | undefined;
|
||||
if (sendParams.length === 1 && firstSendParam.isStoryReply) {
|
||||
const { gif, sticker, isReaction } = firstSendParam;
|
||||
|
||||
if (gif) {
|
||||
storySendMessage = { key: 'ToastTitleMessageSent' };
|
||||
} else if (sticker) {
|
||||
storySendMessage = { key: 'StoryTooltipStickerSent' };
|
||||
} else if (isReaction) {
|
||||
storySendMessage = { key: 'StoryTooltipReactionSent' };
|
||||
}
|
||||
}
|
||||
|
||||
const titleKey: RegularLangFnParameters = storySendMessage || (messagesCount === 1 ? { key: 'ToastTitleMessageSent' }
|
||||
: { key: 'ToastTitleMessagesSent', variables: { count: messagesCount } });
|
||||
|
||||
// eslint-disable-next-line eslint-multitab-tt/no-getactions-in-actions
|
||||
getActions().showNotification({
|
||||
localId: getMessageKey(firstMessage),
|
||||
title: titleKey,
|
||||
message: { key: 'ToastMessageSent', variables: { amount: starsForOneMessage * messagesCount } },
|
||||
actionText: { key: 'ButtonUndo' },
|
||||
action: {
|
||||
action: 'deleteMessages',
|
||||
payload: { messageList: firstSendParam.messageList, messageIds: messageIdsForUndo, shouldDeleteForAll: true },
|
||||
},
|
||||
dismissAction: {
|
||||
action: 'sendMessages',
|
||||
payload: {
|
||||
sendParams,
|
||||
},
|
||||
},
|
||||
duration: PAID_SEND_DELAY,
|
||||
shouldShowTimer: true,
|
||||
disableClickDismiss: true,
|
||||
icon: 'star',
|
||||
shouldUseCustomIcon: true,
|
||||
type: 'paidMessage',
|
||||
});
|
||||
}
|
||||
|
||||
addActionHandler('sendMessages', async (global, actions, payload): Promise<void> => {
|
||||
const { sendParams } = payload;
|
||||
await Promise.all(sendParams.map(async (params) => {
|
||||
if (params.forwardedLocalMessagesSlice && params.forwardParams) {
|
||||
await rafPromise();
|
||||
await callApi('forwardApiMessages', params.forwardParams);
|
||||
} else {
|
||||
await sendMessage(global, params);
|
||||
}
|
||||
}));
|
||||
if (sendParams.length > 0 && sendParams[0].messagePriceInStars) actions.loadStarStatus();
|
||||
});
|
||||
|
||||
addActionHandler('loadPinnedMessages', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, threadId } = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
|
||||
@ -408,6 +408,7 @@ addActionHandler('loadPrivacySettings', async (global): Promise<void> => {
|
||||
callApi('fetchPrivacySettings', 'bio'),
|
||||
callApi('fetchPrivacySettings', 'birthday'),
|
||||
callApi('fetchPrivacySettings', 'gifts'),
|
||||
callApi('fetchPrivacySettings', 'noPaidMessages'),
|
||||
]);
|
||||
|
||||
if (result.some((e) => e === undefined)) {
|
||||
@ -427,6 +428,7 @@ addActionHandler('loadPrivacySettings', async (global): Promise<void> => {
|
||||
bioSettings,
|
||||
birthdaySettings,
|
||||
giftsSettings,
|
||||
noPaidMessagesSettings,
|
||||
] = result as {
|
||||
rules: ApiPrivacySettings;
|
||||
}[];
|
||||
@ -450,6 +452,7 @@ addActionHandler('loadPrivacySettings', async (global): Promise<void> => {
|
||||
bio: bioSettings.rules,
|
||||
birthday: birthdaySettings.rules,
|
||||
gifts: giftsSettings.rules,
|
||||
noPaidMessages: noPaidMessagesSettings.rules,
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -703,14 +706,24 @@ addActionHandler('updateGlobalPrivacySettings', async (global, actions, payload)
|
||||
const shouldHideReadMarks = payload.shouldHideReadMarks ?? Boolean(global.settings.byKey.shouldHideReadMarks);
|
||||
const shouldNewNonContactPeersRequirePremium = payload.shouldNewNonContactPeersRequirePremium
|
||||
?? Boolean(global.settings.byKey.shouldNewNonContactPeersRequirePremium);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const nonContactPeersPaidStars = payload.nonContactPeersPaidStars === null ? undefined
|
||||
: payload.nonContactPeersPaidStars || global.settings.byKey.nonContactPeersPaidStars;
|
||||
|
||||
global = replaceSettings(global, { shouldArchiveAndMuteNewNonContact, shouldHideReadMarks });
|
||||
global = getGlobal();
|
||||
global = replaceSettings(global, {
|
||||
shouldArchiveAndMuteNewNonContact,
|
||||
shouldHideReadMarks,
|
||||
shouldNewNonContactPeersRequirePremium,
|
||||
nonContactPeersPaidStars,
|
||||
});
|
||||
setGlobal(global);
|
||||
|
||||
const result = await callApi('updateGlobalPrivacySettings', {
|
||||
shouldArchiveAndMuteNewNonContact,
|
||||
shouldHideReadMarks,
|
||||
shouldNewNonContactPeersRequirePremium,
|
||||
nonContactPeersPaidStars,
|
||||
});
|
||||
|
||||
global = getGlobal();
|
||||
@ -722,6 +735,9 @@ addActionHandler('updateGlobalPrivacySettings', async (global, actions, payload)
|
||||
shouldNewNonContactPeersRequirePremium: !result
|
||||
? !shouldNewNonContactPeersRequirePremium
|
||||
: result.shouldNewNonContactPeersRequirePremium,
|
||||
nonContactPeersPaidStars: !result
|
||||
? undefined
|
||||
: result.nonContactPeersPaidStars,
|
||||
});
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
@ -184,6 +184,26 @@ addActionHandler('loadCommonChats', async (global, actions, payload): Promise<vo
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('addNoPaidMessagesException', async (global, actions, payload): Promise<void> => {
|
||||
const { userId, shouldRefundCharged } = payload;
|
||||
const user = selectUser(global, userId);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await callApi('addNoPaidMessagesException',
|
||||
{ user, shouldRefundCharged });
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
global = updateUserFullInfo(global, userId, {
|
||||
settings: undefined,
|
||||
});
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('updateContact', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
userId, isMuted = false, firstName, lastName, shouldSharePhoneNumber,
|
||||
|
||||
@ -62,6 +62,7 @@ import {
|
||||
selectIsRightColumnShown,
|
||||
selectIsViewportNewest,
|
||||
selectMessageIdsByGroupId,
|
||||
selectPeer,
|
||||
selectPinnedIds,
|
||||
selectReplyStack,
|
||||
selectRequestedChatTranslationLanguage,
|
||||
@ -71,6 +72,7 @@ import {
|
||||
selectThreadInfo,
|
||||
selectViewportIds,
|
||||
} from '../../selectors';
|
||||
import { getPeerStarsForMessage } from '../api/messages';
|
||||
|
||||
import { getIsMobile } from '../../../hooks/useAppLayout';
|
||||
|
||||
@ -1093,3 +1095,39 @@ addActionHandler('closeSharePreparedMessageModal', (global, actions, payload): A
|
||||
sharePreparedMessageModal: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('updateSharePreparedMessageModalSendArgs', async (global, actions, payload): Promise<void> => {
|
||||
const { args, tabId = getCurrentTabId() } = payload || {};
|
||||
const tabState = selectTabState(global, tabId);
|
||||
|
||||
if (!tabState.sharePreparedMessageModal) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args) {
|
||||
global = updateTabState(global, {
|
||||
sharePreparedMessageModal: {
|
||||
...tabState.sharePreparedMessageModal,
|
||||
pendingSendArgs: undefined,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
return;
|
||||
}
|
||||
|
||||
const peer = selectPeer(global, args.peerId);
|
||||
const starsForSendMessage = peer ? await getPeerStarsForMessage(global, peer) : undefined;
|
||||
|
||||
global = getGlobal();
|
||||
global = updateTabState(global, {
|
||||
sharePreparedMessageModal: {
|
||||
...tabState.sharePreparedMessageModal,
|
||||
pendingSendArgs: {
|
||||
peerId: args.peerId,
|
||||
threadId: args.threadId,
|
||||
starsForSendMessage,
|
||||
},
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
@ -542,6 +542,21 @@ addActionHandler('hideEffectInComposer', (global, actions, payload): ActionRetur
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('setPaidMessageAutoApprove', (global): ActionReturnType => {
|
||||
global = {
|
||||
...global,
|
||||
settings: {
|
||||
...global.settings,
|
||||
byKey: {
|
||||
...global.settings.byKey,
|
||||
shouldPaidMessageAutoApprove: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('setReactionEffect', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
chatId, threadId, reaction, tabId = getCurrentTabId(),
|
||||
|
||||
@ -135,3 +135,19 @@ addActionHandler('resetGiftProfileFilter', (global, actions, payload): ActionRet
|
||||
peerId, shouldRefresh: true, tabId: tabState.id,
|
||||
});
|
||||
});
|
||||
|
||||
addActionHandler('openPaymentMessageConfirmDialogOpen', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
return updateTabState(global, {
|
||||
isPaymentMessageConfirmDialogOpen: true,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('closePaymentMessageConfirmDialogOpen', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
return updateTabState(global, {
|
||||
isPaymentMessageConfirmDialogOpen: false,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
@ -9,6 +9,7 @@ import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
import { addStoriesForPeer } from '../../reducers';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import {
|
||||
selectChat,
|
||||
selectCurrentViewedStory,
|
||||
selectPeer,
|
||||
selectPeerFirstStoryId,
|
||||
@ -18,6 +19,7 @@ import {
|
||||
selectTabState,
|
||||
} from '../../selectors';
|
||||
import { fetchChatByUsername } from '../api/chats';
|
||||
import { getPeerStarsForMessage } from '../api/messages';
|
||||
|
||||
addActionHandler('openStoryViewer', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
@ -289,12 +291,16 @@ addActionHandler('copyStoryLink', async (global, actions, payload): Promise<void
|
||||
});
|
||||
});
|
||||
|
||||
addActionHandler('sendMessage', (global, actions, payload): ActionReturnType => {
|
||||
addActionHandler('sendMessage', async (global, actions, payload): Promise<void> => {
|
||||
const { tabId = getCurrentTabId() } = payload;
|
||||
const { storyId, peerId: storyPeerId } = selectCurrentViewedStory(global, tabId);
|
||||
const isStoryReply = Boolean(storyId && storyPeerId);
|
||||
|
||||
if (!isStoryReply) {
|
||||
const chat = storyPeerId ? selectChat(global, storyPeerId) : undefined;
|
||||
if (!chat) return;
|
||||
const messagePriceInStars = await getPeerStarsForMessage(global, chat);
|
||||
|
||||
if (!isStoryReply || messagePriceInStars) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -271,6 +271,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
shouldSuggestStickers: true,
|
||||
shouldSuggestCustomEmoji: true,
|
||||
shouldSkipWebAppCloseConfirmation: false,
|
||||
shouldPaidMessageAutoApprove: false,
|
||||
shouldUpdateStickerSetOrder: true,
|
||||
language: 'en',
|
||||
timeFormat: '24h',
|
||||
@ -278,6 +279,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
isConnectionStatusMinimized: true,
|
||||
shouldArchiveAndMuteNewNonContact: false,
|
||||
shouldNewNonContactPeersRequirePremium: false,
|
||||
nonContactPeersPaidStars: 0,
|
||||
shouldHideReadMarks: false,
|
||||
canTranslate: false,
|
||||
canTranslateChats: true,
|
||||
@ -423,4 +425,6 @@ export const INITIAL_TAB_STATE: TabState = {
|
||||
requestedTranslations: {
|
||||
byChatId: {},
|
||||
},
|
||||
|
||||
isPaymentMessageConfirmDialogOpen: false,
|
||||
};
|
||||
|
||||
@ -73,7 +73,7 @@ import {
|
||||
selectIsChatWithSelf,
|
||||
selectRequestedChatTranslationLanguage,
|
||||
} from './chats';
|
||||
import { selectPeer } from './peers';
|
||||
import { selectPeer, selectPeerPaidMessagesStars } from './peers';
|
||||
import { selectPeerStory } from './stories';
|
||||
import { selectIsStickerFavorite } from './symbols';
|
||||
import { selectTabState } from './tabs';
|
||||
@ -1331,12 +1331,21 @@ export function selectShouldSchedule<T extends GlobalState>(
|
||||
return selectCurrentMessageList(global, tabId)?.type === 'scheduled';
|
||||
}
|
||||
|
||||
export function selectCanSchedule<T extends GlobalState>(
|
||||
global: T,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
const chatId = selectCurrentMessageList(global, tabId)?.chatId;
|
||||
const paidMessagesStars = chatId ? selectPeerPaidMessagesStars(global, chatId) : undefined;
|
||||
return !paidMessagesStars;
|
||||
}
|
||||
|
||||
export function selectCanScheduleUntilOnline<T extends GlobalState>(global: T, id: string) {
|
||||
const isChatWithSelf = selectIsChatWithSelf(global, id);
|
||||
const chatBot = selectBot(global, id);
|
||||
return Boolean(
|
||||
!isChatWithSelf && !chatBot && isUserId(id) && selectUserStatus(global, id)?.wasOnline,
|
||||
);
|
||||
const paidMessagesStars = selectPeerPaidMessagesStars(global, id);
|
||||
return Boolean(!paidMessagesStars
|
||||
&& !isChatWithSelf && !chatBot && isUserId(id) && selectUserStatus(global, id)?.wasOnline);
|
||||
}
|
||||
|
||||
export function selectCustomEmojis(message: ApiMessage) {
|
||||
|
||||
@ -3,10 +3,10 @@ import type { GlobalState, TabArgs } from '../types';
|
||||
|
||||
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../config';
|
||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||
import { isDeletedUser } from '../helpers';
|
||||
import { isChatAdmin, isDeletedUser, isUserId } from '../helpers';
|
||||
import { selectChat, selectChatFullInfo } from './chats';
|
||||
import { selectTabState } from './tabs';
|
||||
import { selectBot, selectUser } from './users';
|
||||
import { selectBot, selectUser, selectUserFullInfo } from './users';
|
||||
|
||||
export function selectPeer<T extends GlobalState>(global: T, peerId: string): ApiPeer | undefined {
|
||||
return selectUser(global, peerId) || selectChat(global, peerId);
|
||||
@ -34,3 +34,19 @@ export function selectPeerSavedGifts<T extends GlobalState>(
|
||||
) : ApiSavedGifts {
|
||||
return selectTabState(global, tabId).savedGifts.giftsByPeerId[peerId];
|
||||
}
|
||||
|
||||
export function selectPeerPaidMessagesStars<T extends GlobalState>(
|
||||
global: T,
|
||||
peerId: string,
|
||||
) {
|
||||
const isChatWithUser = isUserId(peerId);
|
||||
if (isChatWithUser) {
|
||||
const userFullInfo = isChatWithUser ? selectUserFullInfo(global, peerId) : undefined;
|
||||
return userFullInfo?.paidMessagesStars;
|
||||
}
|
||||
|
||||
const chat = selectChat(global, peerId);
|
||||
if (!chat) return undefined;
|
||||
if (isChatAdmin(chat)) return undefined;
|
||||
return chat.paidMessagesStars;
|
||||
}
|
||||
|
||||
@ -24,6 +24,10 @@ export function selectNewNoncontactPeersRequirePremium<T extends GlobalState>(gl
|
||||
return global.settings.byKey.shouldNewNonContactPeersRequirePremium;
|
||||
}
|
||||
|
||||
export function selectNonContactPeersPaidStars<T extends GlobalState>(global: T) {
|
||||
return global.settings.byKey.nonContactPeersPaidStars;
|
||||
}
|
||||
|
||||
export function selectShouldHideReadMarks<T extends GlobalState>(global: T) {
|
||||
return global.settings.byKey.shouldHideReadMarks;
|
||||
}
|
||||
|
||||
@ -8,7 +8,6 @@ import type {
|
||||
ApiChatlistInvite,
|
||||
ApiChatReactions,
|
||||
ApiChatType,
|
||||
ApiContact,
|
||||
ApiDraft,
|
||||
ApiExportedInvite,
|
||||
ApiFormattedText,
|
||||
@ -23,10 +22,10 @@ import type {
|
||||
ApiMessage,
|
||||
ApiMessageEntity,
|
||||
ApiMessageSearchContext,
|
||||
ApiNewPoll,
|
||||
ApiNotification,
|
||||
ApiNotifyPeerType,
|
||||
ApiPaymentStatus,
|
||||
ApiPeer,
|
||||
ApiPhoto,
|
||||
ApiPremiumSection,
|
||||
ApiPreparedInlineMessage,
|
||||
@ -80,6 +79,7 @@ import type {
|
||||
Point,
|
||||
ProfileTabType,
|
||||
ScrollTargetPosition,
|
||||
SendMessageParams,
|
||||
SettingsScreens,
|
||||
SharedMediaType,
|
||||
Size,
|
||||
@ -441,25 +441,10 @@ export interface ActionPayloads {
|
||||
onLoaded?: NoneToVoidFunction;
|
||||
onError?: NoneToVoidFunction;
|
||||
} & WithTabId;
|
||||
sendMessage: {
|
||||
text?: string;
|
||||
entities?: ApiMessageEntity[];
|
||||
attachments?: ApiAttachment[];
|
||||
sticker?: ApiSticker;
|
||||
isSilent?: boolean;
|
||||
scheduledAt?: number;
|
||||
gif?: ApiVideo;
|
||||
poll?: ApiNewPoll;
|
||||
contact?: Partial<ApiContact>;
|
||||
shouldUpdateStickerSetOrder?: boolean;
|
||||
shouldGroupMessages?: boolean;
|
||||
messageList?: MessageList;
|
||||
isReaction?: true; // Reaction to the story are sent in the form of a message
|
||||
isInvertedMedia?: true;
|
||||
effectId?: string;
|
||||
webPageMediaSize?: WebPageMediaSize;
|
||||
webPageUrl?: string;
|
||||
} & WithTabId;
|
||||
sendMessage: Partial<SendMessageParams> & WithTabId;
|
||||
sendMessages: {
|
||||
sendParams: SendMessageParams[];
|
||||
};
|
||||
sendInviteMessages: {
|
||||
chatId: string;
|
||||
userIds: string[];
|
||||
@ -478,7 +463,9 @@ export interface ActionPayloads {
|
||||
deleteMessages: {
|
||||
messageIds: number[];
|
||||
shouldDeleteForAll?: boolean;
|
||||
messageList?: MessageList;
|
||||
} & WithTabId;
|
||||
resetLocalPaidMessages: WithTabId | undefined;
|
||||
deleteParticipantHistory: {
|
||||
peerId: string;
|
||||
chatId: string;
|
||||
@ -543,6 +530,12 @@ export interface ActionPayloads {
|
||||
message: ApiPreparedInlineMessage;
|
||||
} & WithTabId;
|
||||
closeSharePreparedMessageModal: WithTabId | undefined;
|
||||
updateSharePreparedMessageModalSendArgs: {
|
||||
args?: {
|
||||
peerId: string;
|
||||
threadId?: ThreadId;
|
||||
};
|
||||
} & WithTabId;
|
||||
openPreviousReportAdModal: WithTabId | undefined;
|
||||
openPreviousReportModal: WithTabId | undefined;
|
||||
closeReportAdModal: WithTabId | undefined;
|
||||
@ -1744,6 +1737,10 @@ export interface ActionPayloads {
|
||||
isMuted?: boolean;
|
||||
shouldSharePhoneNumber?: boolean;
|
||||
} & WithTabId;
|
||||
addNoPaidMessagesException: {
|
||||
userId: string;
|
||||
shouldRefundCharged: boolean;
|
||||
};
|
||||
loadMoreProfilePhotos: {
|
||||
peerId: string;
|
||||
isPreload?: boolean;
|
||||
@ -1902,7 +1899,18 @@ export interface ActionPayloads {
|
||||
threadId: ThreadId;
|
||||
isSilent?: boolean;
|
||||
scheduledAt?: number;
|
||||
paidMessagesStars?: number;
|
||||
} & WithTabId;
|
||||
sendInlineBotApiResult: {
|
||||
chat: ApiChat;
|
||||
id: string;
|
||||
queryId: string;
|
||||
replyInfo?: ApiInputMessageReplyInfo;
|
||||
sendAs?: ApiPeer;
|
||||
isSilent?: boolean;
|
||||
scheduledAt?: number;
|
||||
allowPaidStars?: number;
|
||||
};
|
||||
resetInlineBot: {
|
||||
username: string;
|
||||
force?: boolean;
|
||||
@ -2154,6 +2162,7 @@ export interface ActionPayloads {
|
||||
|
||||
requestEffectInComposer: WithTabId;
|
||||
hideEffectInComposer: WithTabId;
|
||||
setPaidMessageAutoApprove: undefined;
|
||||
|
||||
updateArchiveSettings: {
|
||||
isMinimized?: boolean;
|
||||
@ -2304,6 +2313,7 @@ export interface ActionPayloads {
|
||||
shouldArchiveAndMuteNewNonContact?: boolean;
|
||||
shouldHideReadMarks?: boolean;
|
||||
shouldNewNonContactPeersRequirePremium?: boolean;
|
||||
nonContactPeersPaidStars?: number | null;
|
||||
};
|
||||
|
||||
// Premium
|
||||
@ -2475,6 +2485,9 @@ export interface ActionPayloads {
|
||||
status: ApiPaymentStatus;
|
||||
} & WithTabId;
|
||||
|
||||
openPaymentMessageConfirmDialogOpen: WithTabId | undefined;
|
||||
closePaymentMessageConfirmDialogOpen: WithTabId | undefined;
|
||||
|
||||
// Forums
|
||||
toggleForum: {
|
||||
chatId: string;
|
||||
|
||||
@ -278,6 +278,8 @@ export type TabState = {
|
||||
byChatId: Record<string, ManagementState>;
|
||||
};
|
||||
|
||||
isPaymentMessageConfirmDialogOpen: boolean;
|
||||
|
||||
storyViewer: {
|
||||
isRibbonShown?: boolean;
|
||||
isArchivedRibbonShown?: boolean;
|
||||
@ -292,6 +294,7 @@ export type TabState = {
|
||||
// Used for better switch animation between peers.
|
||||
lastViewedByPeerIds?: Record<string, number>;
|
||||
isPrivacyModalOpen?: boolean;
|
||||
isPaymentConfirmDialogOpen?: boolean;
|
||||
isStealthModalOpen?: boolean;
|
||||
viewModal?: {
|
||||
storyId: number;
|
||||
@ -509,6 +512,11 @@ export type TabState = {
|
||||
webAppKey: string;
|
||||
message: ApiPreparedInlineMessage;
|
||||
filter: ApiChatType[];
|
||||
pendingSendArgs?: {
|
||||
peerId: string;
|
||||
threadId?: ThreadId;
|
||||
starsForSendMessage?: number;
|
||||
};
|
||||
};
|
||||
|
||||
webApps: {
|
||||
|
||||
@ -50,7 +50,7 @@ export function useViewTransition(): ViewTransitionController {
|
||||
|
||||
transition.ready.then(() => {
|
||||
setTransitionState('animating');
|
||||
}).catch((e) => {
|
||||
}).catch((e:unknown) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
setTransitionState('skipped');
|
||||
|
||||
@ -1472,8 +1472,10 @@ account.toggleUsername#58d6b376 username:string active:Bool = Bool;
|
||||
account.resolveBusinessChatLink#5492e5ee slug:string = account.ResolvedBusinessChatLinks;
|
||||
account.toggleSponsoredMessages#b9d9a38d enabled:Bool = Bool;
|
||||
account.getCollectibleEmojiStatuses#2e7b4543 hash:long = account.EmojiStatuses;
|
||||
account.addNoPaidMessagesException#6f688aa7 flags:# refund_charged:flags.0?true user_id:InputUser = Bool;
|
||||
users.getUsers#d91a548 id:Vector<InputUser> = Vector<User>;
|
||||
users.getFullUser#b60f5918 id:InputUser = users.UserFull;
|
||||
users.getRequirementsToContact#d89a83a3 id:Vector<InputUser> = Vector<RequirementToContact>;
|
||||
contacts.getContacts#5dd69e12 hash:long = contacts.Contacts;
|
||||
contacts.importContacts#2c800be5 contacts:Vector<InputContact> = contacts.ImportedContacts;
|
||||
contacts.deleteContacts#96a0e00 id:Vector<InputUser> = Updates;
|
||||
|
||||
@ -61,6 +61,7 @@
|
||||
"account.resolveBusinessChatLink",
|
||||
"account.toggleSponsoredMessages",
|
||||
"account.getCollectibleEmojiStatuses",
|
||||
"account.addNoPaidMessagesException",
|
||||
"users.getUsers",
|
||||
"users.getFullUser",
|
||||
"contacts.getContacts",
|
||||
|
||||
@ -103,6 +103,7 @@ $color-message-story-mention-to: #74bcff;
|
||||
--color-voice-transcribe-button: #e8f3ff;
|
||||
--color-voice-transcribe-button-own: #cceebf;
|
||||
|
||||
--color-toast-action: #64D1FF;
|
||||
--color-primary: #{$color-primary};
|
||||
--color-primary-shade: #{color.mix($color-primary, $color-black, 92%)};
|
||||
--color-primary-shade-darker: #{color.mix($color-primary, $color-black, 84%)};
|
||||
|
||||
@ -1,26 +1,36 @@
|
||||
import type { TeactNode } from '../lib/teact/teact';
|
||||
|
||||
import type {
|
||||
ApiAttachment,
|
||||
ApiBotInlineMediaResult,
|
||||
ApiBotInlineResult,
|
||||
ApiBotInlineSwitchPm,
|
||||
ApiBotInlineSwitchWebview,
|
||||
ApiChat,
|
||||
ApiChatInviteImporter,
|
||||
ApiContact,
|
||||
ApiDocument,
|
||||
ApiDraft,
|
||||
ApiExportedInvite,
|
||||
ApiFakeType,
|
||||
ApiFormattedText,
|
||||
ApiInputReplyInfo,
|
||||
ApiLabeledPrice,
|
||||
ApiMediaFormat,
|
||||
ApiMessage,
|
||||
ApiMessageEntity,
|
||||
ApiNewPoll,
|
||||
ApiPeer,
|
||||
ApiPhoto,
|
||||
ApiReaction,
|
||||
ApiReactionWithPaid,
|
||||
ApiStarGiftRegular,
|
||||
ApiStarsSubscription,
|
||||
ApiStarsTransaction,
|
||||
ApiSticker,
|
||||
ApiStickerSet,
|
||||
ApiStory,
|
||||
ApiStorySkipped,
|
||||
ApiThreadInfo,
|
||||
ApiTopic,
|
||||
ApiTypingStatus,
|
||||
@ -114,6 +124,7 @@ export interface ISettings {
|
||||
isConnectionStatusMinimized: boolean;
|
||||
shouldArchiveAndMuteNewNonContact?: boolean;
|
||||
shouldNewNonContactPeersRequirePremium?: boolean;
|
||||
nonContactPeersPaidStars?: number;
|
||||
shouldHideReadMarks?: boolean;
|
||||
canTranslate: boolean;
|
||||
canTranslateChats: boolean;
|
||||
@ -126,6 +137,7 @@ export interface ISettings {
|
||||
shouldDebugExportedSenders?: boolean;
|
||||
shouldWarnAboutSvg?: boolean;
|
||||
shouldSkipWebAppCloseConfirmation: boolean;
|
||||
shouldPaidMessageAutoApprove: boolean;
|
||||
hasContactJoinedNotifications?: boolean;
|
||||
hasWebNotifications: boolean;
|
||||
hasPushNotifications: boolean;
|
||||
@ -191,6 +203,7 @@ export enum SettingsScreens {
|
||||
PrivacyGroupChatsAllowedContacts,
|
||||
PrivacyGroupChatsDeniedContacts,
|
||||
PrivacyBlockedUsers,
|
||||
PrivacyNoPaidMessages,
|
||||
Performance,
|
||||
Folders,
|
||||
FoldersCreateFolder,
|
||||
@ -653,3 +666,63 @@ export type GiftProfileFilterOptions = {
|
||||
shouldIncludeDisplayed: boolean;
|
||||
shouldIncludeHidden: boolean;
|
||||
};
|
||||
|
||||
export type SendMessageParams = {
|
||||
chat?: ApiChat;
|
||||
attachments?: ApiAttachment[];
|
||||
lastMessageId?: number;
|
||||
text?: string;
|
||||
entities?: ApiMessageEntity[];
|
||||
replyInfo?: ApiInputReplyInfo;
|
||||
attachment?: ApiAttachment;
|
||||
sticker?: ApiSticker;
|
||||
story?: ApiStory | ApiStorySkipped;
|
||||
gif?: ApiVideo;
|
||||
poll?: ApiNewPoll;
|
||||
contact?: ApiContact;
|
||||
isSilent?: boolean;
|
||||
scheduledAt?: number;
|
||||
groupedId?: string;
|
||||
noWebPage?: boolean;
|
||||
sendAs?: ApiPeer;
|
||||
shouldGroupMessages?: boolean;
|
||||
shouldUpdateStickerSetOrder?: boolean;
|
||||
wasDrafted?: boolean;
|
||||
isInvertedMedia?: true;
|
||||
effectId?: string;
|
||||
webPageMediaSize?: WebPageMediaSize;
|
||||
webPageUrl?: string;
|
||||
starsAmount?: number;
|
||||
isPending?: true;
|
||||
messageList?: MessageList;
|
||||
isReaction?: true; // Reaction to the story are sent in the form of a message
|
||||
messagePriceInStars?: number;
|
||||
localMessage?: ApiMessage;
|
||||
forwardedLocalMessagesSlice?: ForwardedLocalMessagesSlice;
|
||||
isForwarding?: boolean;
|
||||
forwardParams?: ForwardMessagesParams;
|
||||
isStoryReply?: boolean;
|
||||
};
|
||||
|
||||
export type ForwardedLocalMessagesSlice = {
|
||||
messageIds: number[];
|
||||
localMessages: ApiMessage[];
|
||||
};
|
||||
|
||||
export type ForwardMessagesParams = {
|
||||
fromChat: ApiChat;
|
||||
toChat: ApiChat;
|
||||
toThreadId?: ThreadId;
|
||||
messages: ApiMessage[];
|
||||
isSilent?: boolean;
|
||||
scheduledAt?: number;
|
||||
sendAs?: ApiPeer;
|
||||
withMyScore?: boolean;
|
||||
noAuthors?: boolean;
|
||||
noCaptions?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
wasDrafted?: boolean;
|
||||
lastMessageId?: number;
|
||||
forwardedLocalMessagesSlice?: ForwardedLocalMessagesSlice;
|
||||
messagePriceInStars?: number;
|
||||
};
|
||||
|
||||
82
src/types/language.d.ts
vendored
82
src/types/language.d.ts
vendored
@ -1428,6 +1428,25 @@ export interface LangPair {
|
||||
'GetMoreStarsLinkText': undefined;
|
||||
'StarsGiftCompleted': undefined;
|
||||
'GiftSent': undefined;
|
||||
'PrivacyDescriptionMessagesContactsAndPremium': undefined;
|
||||
'PrivacyChargeForMessages': undefined;
|
||||
'PrivacyDescriptionChargeForMessages': undefined;
|
||||
'RemoveFeeTitle': undefined;
|
||||
'ExceptionTitlePrivacyChargeForMessages': undefined;
|
||||
'ExceptionDescriptionPrivacyChargeForMessages': undefined;
|
||||
'SectionTitleStarsForForMessages': undefined;
|
||||
'SubtitlePrivacyAddUsers': undefined;
|
||||
'PrivacyPaidMessagesValue': undefined;
|
||||
'ButtonBuyStars': undefined;
|
||||
'TitleConfirmPayment': undefined;
|
||||
'ToastTitleMessageSent': undefined;
|
||||
'ButtonUndo': undefined;
|
||||
'ConfirmRemoveMessageFee': undefined;
|
||||
'StoryTooltipGifSent': undefined;
|
||||
'StoryTooltipStickerSent': undefined;
|
||||
'StoryTooltipReactionSent': undefined;
|
||||
'StarsNeededTextSendPaidMessages': undefined;
|
||||
'PaidMessageTransactionTotal': undefined;
|
||||
}
|
||||
|
||||
export interface LangPairWithVariables<V extends unknown = LangVariable> {
|
||||
@ -2273,6 +2292,66 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
|
||||
'stars': V;
|
||||
'link': V;
|
||||
};
|
||||
'SectionDescriptionStarsForForMessages': {
|
||||
'percent': V;
|
||||
'amount': V;
|
||||
};
|
||||
'SubtitlePrivacyUsersCount': {
|
||||
'count': V;
|
||||
};
|
||||
'FirstMessageInPaidMessagesChat': {
|
||||
'user': V;
|
||||
'amount': V;
|
||||
};
|
||||
'ComposerPlaceholderPaidMessage': {
|
||||
'amount': V;
|
||||
};
|
||||
'ComposerPlaceholderPaidReply': {
|
||||
'amount': V;
|
||||
};
|
||||
'ConfirmationModalPaymentForOneMessage': {
|
||||
'user': V;
|
||||
'amount': V;
|
||||
};
|
||||
'ConfirmationModalPaymentForMessages': {
|
||||
'user': V;
|
||||
'price': V;
|
||||
'amount': V;
|
||||
'count': V;
|
||||
};
|
||||
'ButtonPayForMessage': {
|
||||
'count': V;
|
||||
};
|
||||
'ToastTitleMessagesSent': {
|
||||
'count': V;
|
||||
};
|
||||
'ToastMessageSent': {
|
||||
'amount': V;
|
||||
};
|
||||
'ActionPaidOneMessageOutgoing': {
|
||||
'amount': V;
|
||||
};
|
||||
'ActionPaidOneMessageIncoming': {
|
||||
'amount': V;
|
||||
'user': V;
|
||||
};
|
||||
'PaneMessagePaidMessageCharge': {
|
||||
'peer': V;
|
||||
'amount': V;
|
||||
};
|
||||
'ConfirmDialogMessageRemoveFee': {
|
||||
'peer': V;
|
||||
};
|
||||
'ConfirmDialogRemoveFeeRefundStars': {
|
||||
'amount': V;
|
||||
};
|
||||
'DescriptionGiftPaidMessage': {
|
||||
'user': V;
|
||||
'amount': V;
|
||||
};
|
||||
'PaidMessageTransactionDescription': {
|
||||
'percent': V;
|
||||
};
|
||||
}
|
||||
|
||||
export interface LangPairPlural {
|
||||
@ -2549,6 +2628,9 @@ export interface LangPairPluralWithVariables<V extends unknown = LangVariable> {
|
||||
'from': V;
|
||||
'count': V;
|
||||
};
|
||||
'PaidMessageTransaction': {
|
||||
'count': V;
|
||||
};
|
||||
}
|
||||
export type RegularLangKey = keyof LangPair;
|
||||
export type RegularLangKeyWithVariables = keyof LangPairWithVariables;
|
||||
|
||||
@ -12,11 +12,26 @@ export function formatStarsAsText(lang: LangFn, amount: number) {
|
||||
return lang('StarsAmountText', { amount }, { pluralValue: amount });
|
||||
}
|
||||
|
||||
export function formatStarsAsIcon(lang: LangFn, amount: number, options?: { asFont?: boolean; className?: string }) {
|
||||
const { asFont, className } = options || {};
|
||||
export function formatStarsAsIcon(lang: LangFn, amount: number, options?: {
|
||||
asFont?: boolean; className?: string; containerClassName?: string; }) {
|
||||
const { asFont, className, containerClassName } = options || {};
|
||||
const icon = asFont
|
||||
? <Icon name="star" className={buildClassName('star-amount-icon', className)} />
|
||||
: <StarIcon type="gold" className={buildClassName('star-amount-icon', className)} size="adaptive" />;
|
||||
|
||||
if (containerClassName) {
|
||||
return (
|
||||
<span className={containerClassName}>
|
||||
{lang('StarsAmount', { amount }, {
|
||||
withNodes: true,
|
||||
specialReplacement: {
|
||||
[STARS_ICON_PLACEHOLDER]: icon,
|
||||
},
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return lang('StarsAmount', { amount }, {
|
||||
withNodes: true,
|
||||
specialReplacement: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user