From 3055df600bf9c9a53053e43213d9a1beca412f84 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 4 Apr 2025 13:04:13 +0200 Subject: [PATCH] Paid Messages: Follow up (#5790) --- src/api/gramjs/apiBuilders/calls.ts | 4 +- src/api/gramjs/apiBuilders/chats.ts | 10 +-- src/api/gramjs/apiBuilders/peers.ts | 10 +-- src/api/gramjs/methods/users.ts | 11 +++ src/assets/localization/fallback.strings | 5 +- src/bundles/stars.ts | 1 + src/components/common/Avatar.tsx | 7 +- src/components/common/Composer.tsx | 44 +++++++--- src/components/common/DeleteMessageModal.tsx | 2 +- src/components/common/FullNameTitle.tsx | 4 +- src/components/common/PeerChip.tsx | 3 +- .../common/embedded/EmbeddedMessage.tsx | 2 +- .../common/embedded/EmbeddedStory.tsx | 2 +- .../common/embedded/EmbeddedStoryForward.tsx | 2 +- .../left/main/hooks/useChatListEntry.tsx | 2 +- .../left/search/helpers/getSenderName.ts | 2 +- .../settings/PrivacyLockedOption.module.scss | 6 ++ .../left/settings/PrivacyLockedOption.tsx | 10 ++- .../left/settings/PrivacyMessages.tsx | 26 ++++-- src/components/mediaViewer/SenderInfo.tsx | 3 +- src/components/middle/MessageListContent.tsx | 2 +- src/components/middle/composer/AttachMenu.tsx | 24 ++++-- .../middle/composer/MessageInput.tsx | 8 +- .../hooks/usePaidMessageConfirmation.ts | 62 +++++++++----- .../middle/message/ActionMessageText.tsx | 3 +- src/components/middle/message/Message.tsx | 2 +- .../middle/message/StoryMention.tsx | 3 +- .../middle/message/actions/GiveawayPrize.tsx | 2 +- .../middle/message/actions/StarGift.tsx | 4 +- .../middle/message/actions/StarGiftUnique.tsx | 2 +- .../middle/message/actions/SuggestedPhoto.tsx | 3 +- src/components/middle/panes/AudioPlayer.tsx | 3 +- .../middle/panes/HeaderPinnedMessage.tsx | 2 +- .../panes/PaidMessageChargePane.module.scss | 6 -- .../middle/panes/PaidMessageChargePane.tsx | 51 ++--------- .../middle/search/MiddleSearchResult.tsx | 2 +- src/components/modals/ModalContainer.tsx | 5 +- src/components/modals/gift/GiftComposer.tsx | 3 +- src/components/modals/gift/GiftModal.tsx | 4 +- .../modals/gift/info/GiftInfoModal.tsx | 4 +- .../gift/transfer/GiftTransferModal.tsx | 2 +- .../modals/gift/upgrade/GiftUpgradeModal.tsx | 3 +- .../modals/paidReaction/PaidReactionModal.tsx | 3 +- .../SharePreparedMessageModal.tsx | 16 ++-- .../modals/stars/StarsBalanceModal.tsx | 3 +- .../chatRefund/ChatRefundModal.async.tsx | 18 ++++ .../stars/chatRefund/ChatRefundModal.tsx | 84 +++++++++++++++++++ .../modals/stars/gift/StarsGiftModal.tsx | 6 +- .../subscription/StarsSubscriptionItem.tsx | 2 +- .../transaction/StarsTransactionItem.tsx | 2 +- src/components/story/Story.tsx | 3 +- src/components/story/StoryPreview.tsx | 3 +- src/components/story/StoryRibbonButton.tsx | 3 +- src/components/story/StorySettings.tsx | 3 +- src/components/ui/Notification.scss | 2 + src/components/ui/Radio.scss | 8 ++ src/components/ui/Radio.tsx | 3 + src/components/ui/RadioGroup.tsx | 2 + src/global/actions/api/bots.ts | 2 +- src/global/actions/api/messages.ts | 17 ++-- src/global/actions/api/users.ts | 21 +++++ src/global/actions/apiUpdaters/misc.ts | 2 +- src/global/actions/ui/chats.ts | 1 - src/global/actions/ui/messages.ts | 6 +- src/global/actions/ui/misc.ts | 15 +++- src/global/actions/ui/stories.ts | 9 +- src/global/actions/ui/users.ts | 3 + src/global/helpers/chats.ts | 33 +------- src/global/helpers/messages.ts | 26 +----- src/global/helpers/peers.ts | 44 +++++++++- src/global/types/actions.ts | 5 ++ src/global/types/tabState.ts | 4 + src/hooks/useMessageMediaMetadata.ts | 3 +- src/lib/gramjs/tl/apiTl.ts | 1 + src/lib/gramjs/tl/static/api.json | 1 + src/styles/_variables.scss | 1 - src/types/language.d.ts | 3 + src/util/notifications.tsx | 2 +- 78 files changed, 468 insertions(+), 248 deletions(-) create mode 100644 src/components/modals/stars/chatRefund/ChatRefundModal.async.tsx create mode 100644 src/components/modals/stars/chatRefund/ChatRefundModal.tsx diff --git a/src/api/gramjs/apiBuilders/calls.ts b/src/api/gramjs/apiBuilders/calls.ts index f26e48043..3ac157919 100644 --- a/src/api/gramjs/apiBuilders/calls.ts +++ b/src/api/gramjs/apiBuilders/calls.ts @@ -9,7 +9,7 @@ import type { } from '../../../lib/secret-sauce'; import type { ApiGroupCall, ApiPhoneCall } from '../../types'; -import { getApiChatIdFromMtpPeer, isPeerUser } from './peers'; +import { getApiChatIdFromMtpPeer, isMtpPeerUser } from './peers'; export function buildApiGroupCallParticipant(participant: GramJs.GroupCallParticipant): GroupCallParticipant { const { @@ -33,7 +33,7 @@ export function buildApiGroupCallParticipant(participant: GramJs.GroupCallPartic raiseHandRating: raiseHandRating?.toString(), volume, date: new Date(date), - isUser: isPeerUser(peer), + isUser: isMtpPeerUser(peer), id: getApiChatIdFromMtpPeer(peer), video: video ? buildApiGroupCallParticipantVideo(video) : undefined, presentation: presentation ? buildApiGroupCallParticipantVideo(presentation) : undefined, diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index ee5d57470..53756ad92 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -36,8 +36,8 @@ import { buildApiPeerColor, buildApiPeerId, getApiChatIdFromMtpPeer, - isPeerChat, - isPeerUser, + isMtpPeerChat, + isMtpPeerUser, } from './peers'; import { buildApiReaction } from './reactions'; @@ -295,9 +295,9 @@ export function getApiChatTypeFromPeerEntity(peerEntity: GramJs.TypeChat | GramJ } export function getPeerKey(peer: GramJs.TypePeer) { - if (isPeerUser(peer)) { + if (isMtpPeerUser(peer)) { return `user${peer.userId}`; - } else if (isPeerChat(peer)) { + } else if (isMtpPeerChat(peer)) { return `chat${peer.chatId}`; } else { return `chat${peer.channelId}`; @@ -305,7 +305,7 @@ export function getPeerKey(peer: GramJs.TypePeer) { } export function getApiChatTitleFromMtpPeer(peer: GramJs.TypePeer, peerEntity: GramJs.User | GramJs.Chat) { - if (isPeerUser(peer)) { + if (isMtpPeerUser(peer)) { return getUserName(peerEntity as GramJs.User); } else { return (peerEntity as GramJs.Chat).title; diff --git a/src/api/gramjs/apiBuilders/peers.ts b/src/api/gramjs/apiBuilders/peers.ts index 75758363c..67811a6ae 100644 --- a/src/api/gramjs/apiBuilders/peers.ts +++ b/src/api/gramjs/apiBuilders/peers.ts @@ -6,15 +6,15 @@ import type { ApiEmojiStatusType, ApiPeerColor } from '../../types'; import { CHANNEL_ID_LENGTH } from '../../../config'; import { numberToHexColor } from '../../../util/colors'; -export function isPeerUser(peer: GramJs.TypePeer | GramJs.TypeInputPeer): peer is GramJs.PeerUser { +export function isMtpPeerUser(peer: GramJs.TypePeer | GramJs.TypeInputPeer): peer is GramJs.PeerUser { return peer.hasOwnProperty('userId'); } -export function isPeerChat(peer: GramJs.TypePeer | GramJs.TypeInputPeer): peer is GramJs.PeerChat { +export function isMtpPeerChat(peer: GramJs.TypePeer | GramJs.TypeInputPeer): peer is GramJs.PeerChat { return peer.hasOwnProperty('chatId'); } -export function isPeerChannel(peer: GramJs.TypePeer | GramJs.TypeInputPeer): peer is GramJs.PeerChannel { +export function isMtpPeerChannel(peer: GramJs.TypePeer | GramJs.TypeInputPeer): peer is GramJs.PeerChannel { return peer.hasOwnProperty('channelId'); } @@ -34,9 +34,9 @@ export function buildApiPeerId(id: BigInt.BigInteger, type: 'user' | 'chat' | 'c } export function getApiChatIdFromMtpPeer(peer: GramJs.TypePeer | GramJs.TypeInputPeer) { - if (isPeerUser(peer)) { + if (isMtpPeerUser(peer)) { return buildApiPeerId(peer.userId, 'user'); - } else if (isPeerChat(peer)) { + } else if (isMtpPeerChat(peer)) { return buildApiPeerId(peer.chatId, 'chat'); } else { return buildApiPeerId((peer as GramJs.InputPeerChannel).channelId, 'channel'); diff --git a/src/api/gramjs/methods/users.ts b/src/api/gramjs/methods/users.ts index 49f900cfd..8b116dcb9 100644 --- a/src/api/gramjs/methods/users.ts +++ b/src/api/gramjs/methods/users.ts @@ -258,6 +258,17 @@ export async function addNoPaidMessagesException({ user, shouldRefundCharged }: return result; } +export async function fetchPaidMessagesRevenue({ user }: { + user: ApiUser; + shouldRefundCharged?: boolean; +}) { + const result = await invokeRequest(new GramJs.account.GetPaidMessagesRevenue({ + userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser, + })); + if (!result) return undefined; + return result.starsAmount.toJSNumber(); +} + export async function fetchProfilePhotos({ peer, offset = 0, diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 37c4a2f92..599d41800 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1890,4 +1890,7 @@ "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"; \ No newline at end of file +"PaidMessageTransactionTotal" = "Total"; +"DescriptionRestrictedMedia" = "Posting media content is not allowed in this group."; +"DescriptionScheduledPaidMediaNotAllowed" = "Posting scheduled paid media content is not allowed"; +"DescriptionScheduledPaidMessagesNotAllowed" = "Scheduled paid messages is not allowed"; \ No newline at end of file diff --git a/src/bundles/stars.ts b/src/bundles/stars.ts index c0f4ef467..6adb938fb 100644 --- a/src/bundles/stars.ts +++ b/src/bundles/stars.ts @@ -12,3 +12,4 @@ export { default as GiftUpgradeModal } from '../components/modals/gift/upgrade/G export { default as GiftStatusInfoModal } from '../components/modals/gift/status/GiftStatusInfoModal'; export { default as GiftWithdrawModal } from '../components/modals/gift/withdraw/GiftWithdrawModal'; export { default as GiftTransferModal } from '../components/modals/gift/transfer/GiftTransferModal'; +export { default as ChatRefundModal } from '../components/modals/stars/chatRefund/ChatRefundModal'; diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx index 8a35f134b..7feb89515 100644 --- a/src/components/common/Avatar.tsx +++ b/src/components/common/Avatar.tsx @@ -21,10 +21,9 @@ import { isAnonymousForwardsChat, isChatWithRepliesBot, isDeletedUser, - isPeerChat, - isPeerUser, isUserId, } from '../../global/helpers'; +import { isApiPeerChat, isApiPeerUser } from '../../global/helpers/peers'; import buildClassName, { createClassNameBuilder } from '../../util/buildClassName'; import buildStyle from '../../util/buildStyle'; import { getFirstLetters } from '../../util/textFormat'; @@ -117,8 +116,8 @@ const Avatar: FC = ({ const videoLoopCountRef = useRef(0); const isCustomPeer = peer && 'isCustomPeer' in peer; const realPeer = peer && !isCustomPeer ? peer : undefined; - const user = realPeer && isPeerUser(realPeer) ? realPeer : undefined; - const chat = realPeer && isPeerChat(realPeer) ? realPeer : undefined; + const user = realPeer && isApiPeerUser(realPeer) ? realPeer : undefined; + const chat = realPeer && isApiPeerChat(realPeer) ? realPeer : undefined; const isDeleted = user && isDeletedUser(user); const isReplies = realPeer && isChatWithRepliesBot(realPeer.id); const isAnonymousForwards = realPeer && isAnonymousForwardsChat(realPeer.id); diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index 393333d35..d92999757 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -57,7 +57,6 @@ import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterd import { canEditMedia, getAllowedAttachmentOptions, - getPeerTitle, getReactionKey, getStoryKey, isChatAdmin, @@ -68,6 +67,7 @@ import { isUserId, } from '../../global/helpers'; import { getChatNotifySettings } from '../../global/helpers/notifications'; +import { getPeerTitle } from '../../global/helpers/peers'; import { selectBot, selectCanPlayAnimatedEmojis, @@ -288,6 +288,8 @@ type StateProps = shouldPaidMessageAutoApprove?: boolean; isSilentPosting?: boolean; isPaymentMessageConfirmDialogOpen: boolean; + starsBalance: number; + isStarsBalanceModalOpen: boolean; }; enum MainButtonState { @@ -406,6 +408,8 @@ const Composer: FC = ({ onBlur, onForward, isPaymentMessageConfirmDialogOpen, + starsBalance, + isStarsBalanceModalOpen, }) => { const { sendMessage, @@ -520,8 +524,13 @@ const Composer: FC = ({ canSendStickers, canSendGifs, canAttachMedia, canAttachPolls, canAttachEmbedLinks, canSendVoices, canSendPlainText, canSendAudios, canSendVideos, canSendPhotos, canSendDocuments, } = useMemo( - () => getAllowedAttachmentOptions(chat, chatFullInfo, isChatWithBot, isInStoryViewer), - [chat, chatFullInfo, isChatWithBot, isInStoryViewer], + () => getAllowedAttachmentOptions(chat, + chatFullInfo, + isChatWithBot, + isInStoryViewer, + paidMessagesStars, + isInScheduledList), + [chat, chatFullInfo, isChatWithBot, isInStoryViewer, paidMessagesStars, isInScheduledList], ); const isNeedPremium = isContactRequirePremium && isInStoryViewer; @@ -541,7 +550,7 @@ const Composer: FC = ({ shouldAutoApprove: shouldPaidMessageAutoApprove, setAutoApprove: setShouldPaidMessageAutoApprove, handleWithConfirmation: handleActionWithPaymentConfirmation, - } = usePaidMessageConfirmation(starsForAllMessages); + } = usePaidMessageConfirmation(starsForAllMessages, isStarsBalanceModalOpen, starsBalance); const hasWebPagePreview = !hasAttachments && canAttachEmbedLinks && !noWebPage && Boolean(webPagePreview); const isComposerBlocked = isSendTextBlocked && !editingMessage; @@ -1391,19 +1400,23 @@ const Composer: FC = ({ if (isInScheduledList) { requestCalendar((scheduledAt) => { - handleMessageSchedule({ poll }, scheduledAt, currentMessageList); + handleActionWithPaymentConfirmation( + handleMessageSchedule, + { poll }, + scheduledAt, + currentMessageList, + ); }); closePollModal(); } else { - sendMessage({ messageList: currentMessageList, poll, isSilent: isSilentPosting }); + handleActionWithPaymentConfirmation( + sendMessage, + { messageList: currentMessageList, poll, isSilent: isSilentPosting }, + ); closePollModal(); } }); - const handlePollSendWithPaymentConfirmation = useLastCallback((poll: ApiNewPoll) => { - handleActionWithPaymentConfirmation(handlePollSend, poll); - }); - const sendSilent = useLastCallback((additionalArgs?: ScheduledMessageArgs) => { if (isInScheduledList) { requestCalendar((scheduledAt) => { @@ -1587,7 +1600,7 @@ const Composer: FC = ({ message: oldLang('VoiceMessagesRestrictedByPrivacy', chat?.title), }); } else if (!canSendVoices) { - showAllowedMessageTypesNotification({ chatId }); + showAllowedMessageTypesNotification({ chatId, messageListType }); } } else { setIsViewOnceEnabled(false); @@ -1812,7 +1825,7 @@ const Composer: FC = ({ isQuiz={pollModal.isQuiz} shouldBeAnonymous={isChannel} onClear={closePollModal} - onSend={handlePollSendWithPaymentConfirmation} + onSend={handlePollSend} /> = ({ onFocus={markInputHasFocus} onBlur={unmarkInputHasFocus} isNeedPremium={isNeedPremium} + messageListType={messageListType} /> {isInMessageList && ( <> @@ -2084,6 +2098,8 @@ const Composer: FC = ({ theme={theme} onMenuOpen={onAttachMenuOpen} onMenuClose={onAttachMenuClose} + messageListType={messageListType} + paidMessagesStars={paidMessagesStars} /> )} {isInMessageList && Boolean(botKeyboardMessageId) && ( @@ -2354,6 +2370,8 @@ export default memo(withGlobal( const maxMessageLength = global.config?.maxMessageLength || DEFAULT_MAX_MESSAGE_LENGTH; const isForwarding = chatId === tabState.forwardMessages.toChatId; + const starsBalance = global.stars?.balance.amount || 0; + const isStarsBalanceModalOpen = Boolean(tabState.starsBalanceModal); return { availableReactions: global.reactions.availableReactions, @@ -2436,6 +2454,8 @@ export default memo(withGlobal( shouldPaidMessageAutoApprove, isSilentPosting, isPaymentMessageConfirmDialogOpen: tabState.isPaymentMessageConfirmDialogOpen, + starsBalance, + isStarsBalanceModalOpen, }; }, )(Composer)); diff --git a/src/components/common/DeleteMessageModal.tsx b/src/components/common/DeleteMessageModal.tsx index 2e68590e9..47f823c8c 100644 --- a/src/components/common/DeleteMessageModal.tsx +++ b/src/components/common/DeleteMessageModal.tsx @@ -12,7 +12,6 @@ import type { IRadioOption } from '../ui/CheckboxGroup'; import { getHasAdminRight, - getPeerTitle, getPrivateChatUserId, getUserFirstOrLastName, isChatBasicGroup, isChatChannel, @@ -20,6 +19,7 @@ import { isSystemBot, isUserId, } from '../../global/helpers'; +import { getPeerTitle } from '../../global/helpers/peers'; import { getSendersFromSelectedMessages, selectBot, diff --git a/src/components/common/FullNameTitle.tsx b/src/components/common/FullNameTitle.tsx index ddc3acc7c..5b18180a9 100644 --- a/src/components/common/FullNameTitle.tsx +++ b/src/components/common/FullNameTitle.tsx @@ -15,8 +15,8 @@ import { isAnonymousForwardsChat, isChatWithRepliesBot, isChatWithVerificationCodesBot, - isPeerUser, } from '../../global/helpers'; +import { isApiPeerUser } from '../../global/helpers/peers'; import buildClassName from '../../util/buildClassName'; import buildStyle from '../../util/buildStyle'; import { copyTextToClipboard } from '../../util/clipboard'; @@ -71,7 +71,7 @@ const FullNameTitle: FC = ({ const { showNotification } = getActions(); const realPeer = 'id' in peer ? peer : undefined; const customPeer = 'isCustomPeer' in peer ? peer : undefined; - const isUser = realPeer && isPeerUser(realPeer); + const isUser = realPeer && isApiPeerUser(realPeer); const title = realPeer && (isUser ? getUserFullName(realPeer) : getChatTitle(lang, realPeer)); const isPremium = isUser && realPeer.isPremium; const canShowEmojiStatus = withEmojiStatus && !isSavedMessages && realPeer; diff --git a/src/components/common/PeerChip.tsx b/src/components/common/PeerChip.tsx index 86e7317df..5a7709610 100644 --- a/src/components/common/PeerChip.tsx +++ b/src/components/common/PeerChip.tsx @@ -6,8 +6,7 @@ import type { ApiPeer } from '../../api/types'; import type { CustomPeer } from '../../types'; import type { IconName } from '../../types/icons'; -import { getPeerTitle } from '../../global/helpers'; -import { isApiPeerChat } from '../../global/helpers/peers'; +import { getPeerTitle, isApiPeerChat } from '../../global/helpers/peers'; import { selectPeer, selectUser } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import { getPeerColorClass } from './helpers/peerColor'; diff --git a/src/components/common/embedded/EmbeddedMessage.tsx b/src/components/common/embedded/EmbeddedMessage.tsx index fb59074ef..3937166b2 100644 --- a/src/components/common/embedded/EmbeddedMessage.tsx +++ b/src/components/common/embedded/EmbeddedMessage.tsx @@ -14,13 +14,13 @@ import { getMessageIsSpoiler, getMessageMediaHash, getMessageRoundVideo, - getPeerTitle, isChatChannel, isChatGroup, isMessageTranslatable, isUserId, } from '../../../global/helpers'; import { getMediaContentTypeDescription } from '../../../global/helpers/messageSummary'; +import { getPeerTitle } from '../../../global/helpers/peers'; import buildClassName from '../../../util/buildClassName'; import freezeWhenClosed from '../../../util/hoc/freezeWhenClosed'; import { getPictogramDimensions } from '../helpers/mediaDimensions'; diff --git a/src/components/common/embedded/EmbeddedStory.tsx b/src/components/common/embedded/EmbeddedStory.tsx index f45941fec..b3d1d2076 100644 --- a/src/components/common/embedded/EmbeddedStory.tsx +++ b/src/components/common/embedded/EmbeddedStory.tsx @@ -6,9 +6,9 @@ import type { ApiPeer, ApiTypeStory } from '../../../api/types'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import { - getPeerTitle, getStoryMediaHash, } from '../../../global/helpers'; +import { getPeerTitle } from '../../../global/helpers/peers'; import buildClassName from '../../../util/buildClassName'; import { getPictogramDimensions } from '../helpers/mediaDimensions'; import renderText from '../helpers/renderText'; diff --git a/src/components/common/embedded/EmbeddedStoryForward.tsx b/src/components/common/embedded/EmbeddedStoryForward.tsx index 4f6f3a9be..59430c108 100644 --- a/src/components/common/embedded/EmbeddedStoryForward.tsx +++ b/src/components/common/embedded/EmbeddedStoryForward.tsx @@ -10,9 +10,9 @@ import type { import type { IconName } from '../../../types/icons'; import { - getPeerTitle, isUserId, } from '../../../global/helpers'; +import { getPeerTitle } from '../../../global/helpers/peers'; import { selectPeer, selectPeerStory } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import { getPeerColorClass } from '../helpers/peerColor'; diff --git a/src/components/left/main/hooks/useChatListEntry.tsx b/src/components/left/main/hooks/useChatListEntry.tsx index dadbee0a0..a317784da 100644 --- a/src/components/left/main/hooks/useChatListEntry.tsx +++ b/src/components/left/main/hooks/useChatListEntry.tsx @@ -15,10 +15,10 @@ import { getMessageMediaHash, getMessageMediaThumbDataUri, getMessageRoundVideo, - getMessageSenderName, getMessageSticker, getMessageVideo, } from '../../../../global/helpers'; +import { getMessageSenderName } from '../../../../global/helpers/peers'; import buildClassName from '../../../../util/buildClassName'; import renderText from '../../../common/helpers/renderText'; import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities'; diff --git a/src/components/left/search/helpers/getSenderName.ts b/src/components/left/search/helpers/getSenderName.ts index fd2757624..d814bc334 100644 --- a/src/components/left/search/helpers/getSenderName.ts +++ b/src/components/left/search/helpers/getSenderName.ts @@ -3,10 +3,10 @@ import type { OldLangFn } from '../../../../hooks/useOldLang'; import { getChatTitle, - getPeerTitle, isChatGroup, isUserId, } from '../../../../global/helpers'; +import { getPeerTitle } from '../../../../global/helpers/peers'; export function getSenderName( lang: OldLangFn, message: ApiMessage, chatsById: Record, usersById: Record, diff --git a/src/components/left/settings/PrivacyLockedOption.module.scss b/src/components/left/settings/PrivacyLockedOption.module.scss index a9e452d90..ec0307bbd 100644 --- a/src/components/left/settings/PrivacyLockedOption.module.scss +++ b/src/components/left/settings/PrivacyLockedOption.module.scss @@ -10,3 +10,9 @@ transform: translateY(-50%); color: var(--color-gray); } + +.checked .lock-icon { + left: 1.25rem; + font-size: 1rem; + color: var(--color-primary); +} diff --git a/src/components/left/settings/PrivacyLockedOption.tsx b/src/components/left/settings/PrivacyLockedOption.tsx index c23eca3f7..23861130f 100644 --- a/src/components/left/settings/PrivacyLockedOption.tsx +++ b/src/components/left/settings/PrivacyLockedOption.tsx @@ -1,6 +1,8 @@ import React, { memo } from '../../../lib/teact/teact'; import { getActions } from '../../../global'; +import buildClassName from '../../../util/buildClassName'; + import useOldLang from '../../../hooks/useOldLang'; import Icon from '../../common/icons/Icon'; @@ -9,15 +11,19 @@ import styles from './PrivacyLockedOption.module.scss'; type OwnProps = { label: string; + isChecked?: boolean; }; -function PrivacyLockedOption({ label }: OwnProps) { +function PrivacyLockedOption({ label, isChecked }: OwnProps) { const lang = useOldLang(); const { showNotification } = getActions(); return (
showNotification({ message: lang('OptionPremiumRequiredMessage') })} > {label} diff --git a/src/components/left/settings/PrivacyMessages.tsx b/src/components/left/settings/PrivacyMessages.tsx index fae81b552..835eeb972 100644 --- a/src/components/left/settings/PrivacyMessages.tsx +++ b/src/components/left/settings/PrivacyMessages.tsx @@ -72,6 +72,12 @@ function PrivacyMessages({ const canChangeChargeForMessages = isCurrentUserPremium && canChargeForMessages; const [chargeForMessages, setChargeForMessages] = useState(nonContactPeersPaidStars); + const selectedValue = useMemo(() => { + if (shouldChargeForMessages) return 'charge_for_messages'; + if (shouldNewNonContactPeersRequirePremium) return 'contacts_and_premium'; + return 'everybody'; + }, [shouldChargeForMessages, shouldNewNonContactPeersRequirePremium]); + const options = useMemo(() => { return [ { value: 'everybody', label: oldLang('P2PEverybody') }, @@ -80,21 +86,29 @@ function PrivacyMessages({ label: canChangeForContactsAndPremium ? ( oldLang('PrivacyMessagesContactsAndPremium') ) : ( - + ), hidden: !canChangeForContactsAndPremium, + isCanCheckedInDisabled: true, }, { value: 'charge_for_messages', label: canChangeChargeForMessages ? ( lang('PrivacyChargeForMessages') ) : ( - + ), hidden: !canChangeChargeForMessages, + isCanCheckedInDisabled: true, }, ]; - }, [oldLang, lang, canChangeForContactsAndPremium, canChangeChargeForMessages]); + }, [oldLang, lang, canChangeForContactsAndPremium, canChangeChargeForMessages, selectedValue]); const handleChange = useLastCallback((privacy: string) => { updateGlobalPrivacySettings({ @@ -184,12 +198,6 @@ function PrivacyMessages({ 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'); diff --git a/src/components/mediaViewer/SenderInfo.tsx b/src/components/mediaViewer/SenderInfo.tsx index ef654bc0b..48b70855b 100644 --- a/src/components/mediaViewer/SenderInfo.tsx +++ b/src/components/mediaViewer/SenderInfo.tsx @@ -6,8 +6,9 @@ import type { ApiChat, ApiPeer } from '../../api/types'; import type { MediaViewerItem } from './helpers/getViewableMedia'; import { - getPeerTitle, isChatChannel, isChatGroup, isUserId, + isChatChannel, isChatGroup, isUserId, } from '../../global/helpers'; +import { getPeerTitle } from '../../global/helpers/peers'; import { selectSender, } from '../../global/selectors'; diff --git a/src/components/middle/MessageListContent.tsx b/src/components/middle/MessageListContent.tsx index ff90b6157..57beeb49f 100644 --- a/src/components/middle/MessageListContent.tsx +++ b/src/components/middle/MessageListContent.tsx @@ -14,11 +14,11 @@ import { SCHEDULED_WHEN_ONLINE } from '../../config'; import { getMessageHtmlId, getMessageOriginalId, - getPeerTitle, isActionMessage, isOwnMessage, isServiceNotificationMessage, } from '../../global/helpers'; +import { getPeerTitle } from '../../global/helpers/peers'; import { selectSender } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import { formatHumanDate } from '../../util/dates/dateFormat'; diff --git a/src/components/middle/composer/AttachMenu.tsx b/src/components/middle/composer/AttachMenu.tsx index f79b3fcb5..3db764553 100644 --- a/src/components/middle/composer/AttachMenu.tsx +++ b/src/components/middle/composer/AttachMenu.tsx @@ -6,7 +6,7 @@ import React, { import type { ApiAttachMenuPeerType, ApiMessage } from '../../../api/types'; import type { GlobalState } from '../../../global/types'; -import type { ISettings, ThreadId } from '../../../types'; +import type { ISettings, MessageListType, ThreadId } from '../../../types'; import { CONTENT_TYPES_WITH_PREVIEW, DEBUG_LOG_FILENAME, SUPPORTED_AUDIO_CONTENT_TYPES, @@ -27,6 +27,7 @@ import { openSystemFilesDialog } from '../../../util/systemFilesDialog'; import { IS_TOUCH_ENV } from '../../../util/windowEnvironment'; import useFlag from '../../../hooks/useFlag'; +import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import useMouseInside from '../../../hooks/useMouseInside'; import useOldLang from '../../../hooks/useOldLang'; @@ -60,6 +61,8 @@ export type OwnProps = { onMenuClose: NoneToVoidFunction; canEditMedia?: boolean; editingMessage?: ApiMessage; + messageListType?: MessageListType; + paidMessagesStars?: number; }; const AttachMenu: FC = ({ @@ -83,6 +86,8 @@ const AttachMenu: FC = ({ onPollCreate, canEditMedia, editingMessage, + messageListType, + paidMessagesStars, }) => { const [isAttachMenuOpen, openAttachMenu, closeAttachMenu] = useFlag(); const [handleMouseEnter, handleMouseLeave, markMouseInside] = useMouseInside(isAttachMenuOpen, closeAttachMenu); @@ -164,7 +169,8 @@ const AttachMenu: FC = ({ : undefined; }, [attachBots, chatId, peerType]); - const lang = useOldLang(); + const oldLang = useOldLang(); + const lang = useLang(); if (!isButtonVisible) { return undefined; @@ -221,31 +227,35 @@ const AttachMenu: FC = ({ ** transferring to the fragment content in the second clause */} {!canAttachMedia && ( - Posting media content is not allowed in this group. + + {lang(messageListType === 'scheduled' && paidMessagesStars + ? 'DescriptionScheduledPaidMediaNotAllowed' + : 'DescriptionRestrictedMedia')} + )} {canAttachMedia && ( <> {canSendVideoOrPhoto && !isFile && ( - {lang(canSendVideoAndPhoto ? 'AttachmentMenu.PhotoOrVideo' + {oldLang(canSendVideoAndPhoto ? 'AttachmentMenu.PhotoOrVideo' : (canSendPhotos ? 'InputAttach.Popover.Photo' : 'InputAttach.Popover.Video'))} )} {((canSendDocuments || canSendAudios) && !isPhotoOrVideo) && ( - {lang(!canSendDocuments && canSendAudios ? 'InputAttach.Popover.Music' : 'AttachDocument')} + {oldLang(!canSendDocuments && canSendAudios ? 'InputAttach.Popover.Music' : 'AttachDocument')} )} {canSendDocuments && shouldCollectDebugLogs && ( - {lang('DebugSendLogs')} + {oldLang('DebugSendLogs')} )} )} {canAttachPolls && !editingMessage && ( - {lang('Poll')} + {oldLang('Poll')} )} {!editingMessage && !canEditMedia && !isScheduled && bots?.map((bot) => ( diff --git a/src/components/middle/composer/MessageInput.tsx b/src/components/middle/composer/MessageInput.tsx index 182f7ad4b..a92473c6b 100644 --- a/src/components/middle/composer/MessageInput.tsx +++ b/src/components/middle/composer/MessageInput.tsx @@ -8,7 +8,9 @@ import React, { import { getActions, withGlobal } from '../../../global'; import type { ApiInputMessageReplyInfo } from '../../../api/types'; -import type { IAnchorPosition, ISettings, ThreadId } from '../../../types'; +import type { + IAnchorPosition, ISettings, MessageListType, ThreadId, +} from '../../../types'; import type { Signal } from '../../../util/signals'; import { EDITABLE_INPUT_ID } from '../../../config'; @@ -75,6 +77,7 @@ type OwnProps = { onFocus?: NoneToVoidFunction; onBlur?: NoneToVoidFunction; isNeedPremium?: boolean; + messageListType?: MessageListType; }; type StateProps = { @@ -141,6 +144,7 @@ const MessageInput: FC = ({ onFocus, onBlur, isNeedPremium, + messageListType, }) => { const { editLastMessage, @@ -456,7 +460,7 @@ const MessageInput: FC = ({ function handleClick() { if (isAttachmentModalInput || canSendPlainText || (isStoryInput && isNeedPremium)) return; - showAllowedMessageTypesNotification({ chatId }); + showAllowedMessageTypesNotification({ chatId, messageListType }); } const handleOpenPremiumModal = useLastCallback(() => openPremiumModal()); diff --git a/src/components/middle/composer/hooks/usePaidMessageConfirmation.ts b/src/components/middle/composer/hooks/usePaidMessageConfirmation.ts index c6aed9912..724be7749 100644 --- a/src/components/middle/composer/hooks/usePaidMessageConfirmation.ts +++ b/src/components/middle/composer/hooks/usePaidMessageConfirmation.ts @@ -1,4 +1,4 @@ -import { useRef, useState } from '../../../../lib/teact/teact'; +import { useEffect, useRef, useState } from '../../../../lib/teact/teact'; import { getActions, getGlobal } from '../../../../global'; import { PAID_MESSAGES_PURPOSE } from '../../../../config'; @@ -7,6 +7,8 @@ import useLastCallback from '../../../../hooks/useLastCallback'; export default function usePaidMessageConfirmation( starsForAllMessages: number, + isStarsBalanceModeOpen: boolean, + starsBalance: number, ) { const { shouldPaidMessageAutoApprove, @@ -14,41 +16,63 @@ export default function usePaidMessageConfirmation( const [shouldAutoApprove, setAutoApprove] = useState(Boolean(shouldPaidMessageAutoApprove)); + const [isWaitingStarsTopup, setIsWaitingStarsTopup] = useState(false); const confirmPaymentHandlerRef = useRef(undefined); const closeConfirmDialog = useLastCallback(() => { getActions().closePaymentMessageConfirmDialogOpen(); }); + useEffect(() => { + if (isWaitingStarsTopup && !isStarsBalanceModeOpen) { + setIsWaitingStarsTopup(false); + + if (starsBalance > starsForAllMessages) { + confirmPaymentHandlerRef?.current?.(); + } + } + }, [isWaitingStarsTopup, isStarsBalanceModeOpen, starsBalance, starsForAllMessages]); + + const handleStarsTopup = useLastCallback(() => { + getActions().openStarsBalanceModal({ + topup: { + balanceNeeded: starsForAllMessages, + purpose: PAID_MESSAGES_PURPOSE, + }, + }); + setIsWaitingStarsTopup(true); + }); + + const dialogHandler = useLastCallback(() => { + if (starsForAllMessages > starsBalance) { + handleStarsTopup(); + } else { + confirmPaymentHandlerRef?.current?.(); + } + getActions().closePaymentMessageConfirmDialogOpen(); + if (shouldAutoApprove) getActions().setPaidMessageAutoApprove(); + }); + const handleWithConfirmation = void>( handler: T, ...args: Parameters ) => { if (starsForAllMessages) { - const balance = getGlobal().stars?.balance.amount; - if (balance && starsForAllMessages > balance) { - getActions().openStarsBalanceModal({ - topup: - { balanceNeeded: starsForAllMessages, purpose: PAID_MESSAGES_PURPOSE }, - }); + confirmPaymentHandlerRef.current = () => handler(...args); + if (!shouldPaidMessageAutoApprove) { + getActions().openPaymentMessageConfirmDialogOpen(); + return; + } + + if (starsForAllMessages > starsBalance) { + handleStarsTopup(); return; } } - if (!shouldPaidMessageAutoApprove && starsForAllMessages) { - confirmPaymentHandlerRef.current = () => handler(...args); - getActions().openPaymentMessageConfirmDialogOpen(); - } else { - handler(...args); - } + handler(...args); }; - const dialogHandler = useLastCallback(() => { - confirmPaymentHandlerRef.current?.(); - getActions().closePaymentMessageConfirmDialogOpen(); - if (shouldAutoApprove) getActions().setPaidMessageAutoApprove(); - }); - return { closeConfirmDialog, handleWithConfirmation, diff --git a/src/components/middle/message/ActionMessageText.tsx b/src/components/middle/message/ActionMessageText.tsx index 1bcea70dd..ee038e453 100644 --- a/src/components/middle/message/ActionMessageText.tsx +++ b/src/components/middle/message/ActionMessageText.tsx @@ -5,8 +5,9 @@ import type { ApiChat, ApiMessage, ApiPeer } from '../../../api/types'; import { GENERAL_TOPIC_ID, SERVICE_NOTIFICATIONS_USER_ID, TME_LINK_PREFIX } from '../../../config'; import { - getMessageInvoice, getMessageText, getPeerTitle, isChatChannel, + getMessageInvoice, getMessageText, isChatChannel, } from '../../../global/helpers'; +import { getPeerTitle } from '../../../global/helpers/peers'; import { getMessageReplyInfo } from '../../../global/helpers/replies'; import { selectChat, diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index 9c6cbc279..44c536bcb 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -53,7 +53,6 @@ import { getMessageHtmlId, getMessageSingleCustomEmoji, getMessageSingleRegularEmoji, - getPeerFullTitle, hasMessageText, hasMessageTtl, isAnonymousForwardsChat, @@ -69,6 +68,7 @@ import { isSystemBot, isUserId, } from '../../../global/helpers'; +import { getPeerFullTitle } from '../../../global/helpers/peers'; import { getMessageReplyInfo, getStoryReplyInfo } from '../../../global/helpers/replies'; import { selectActiveDownloads, diff --git a/src/components/middle/message/StoryMention.tsx b/src/components/middle/message/StoryMention.tsx index e84a6502a..381f5b0ef 100644 --- a/src/components/middle/message/StoryMention.tsx +++ b/src/components/middle/message/StoryMention.tsx @@ -5,7 +5,8 @@ import type { ApiMessage, ApiPeer, ApiTypeStory, ApiUser, } from '../../../api/types'; -import { getPeerTitle, getStoryMediaHash, getUserFirstOrLastName } from '../../../global/helpers'; +import { getStoryMediaHash, getUserFirstOrLastName } from '../../../global/helpers'; +import { getPeerTitle } from '../../../global/helpers/peers'; import { selectPeer, selectPeerStories, diff --git a/src/components/middle/message/actions/GiveawayPrize.tsx b/src/components/middle/message/actions/GiveawayPrize.tsx index d932f6ba7..15e1b4135 100644 --- a/src/components/middle/message/actions/GiveawayPrize.tsx +++ b/src/components/middle/message/actions/GiveawayPrize.tsx @@ -4,7 +4,7 @@ import { withGlobal } from '../../../../global'; import type { ApiChat, ApiSticker } from '../../../../api/types'; import type { ApiMessageActionGiftCode, ApiMessageActionPrizeStars } from '../../../../api/types/messageActions'; -import { getPeerTitle } from '../../../../global/helpers'; +import { getPeerTitle } from '../../../../global/helpers/peers'; import { selectCanPlayAnimatedEmojis, selectChat, diff --git a/src/components/middle/message/actions/StarGift.tsx b/src/components/middle/message/actions/StarGift.tsx index 3f76f324c..dda8e3fff 100644 --- a/src/components/middle/message/actions/StarGift.tsx +++ b/src/components/middle/message/actions/StarGift.tsx @@ -4,8 +4,8 @@ import { withGlobal } from '../../../../global'; import type { ApiMessage, ApiPeer } from '../../../../api/types'; import type { ApiMessageActionStarGift } from '../../../../api/types/messageActions'; -import { getPeerTitle, isChatChannel } from '../../../../global/helpers'; -import { isApiPeerChat } from '../../../../global/helpers/peers'; +import { isChatChannel } from '../../../../global/helpers'; +import { getPeerTitle, isApiPeerChat } from '../../../../global/helpers/peers'; import { selectCanPlayAnimatedEmojis, selectPeer, diff --git a/src/components/middle/message/actions/StarGiftUnique.tsx b/src/components/middle/message/actions/StarGiftUnique.tsx index 1a9d31f6f..c2fe7bb6a 100644 --- a/src/components/middle/message/actions/StarGiftUnique.tsx +++ b/src/components/middle/message/actions/StarGiftUnique.tsx @@ -4,7 +4,7 @@ import { withGlobal } from '../../../../global'; import type { ApiMessage, ApiPeer } from '../../../../api/types'; import type { ApiMessageActionStarGiftUnique } from '../../../../api/types/messageActions'; -import { getPeerTitle } from '../../../../global/helpers'; +import { getPeerTitle } from '../../../../global/helpers/peers'; import { selectCanPlayAnimatedEmojis, selectPeer, diff --git a/src/components/middle/message/actions/SuggestedPhoto.tsx b/src/components/middle/message/actions/SuggestedPhoto.tsx index a986042f3..f25a10adc 100644 --- a/src/components/middle/message/actions/SuggestedPhoto.tsx +++ b/src/components/middle/message/actions/SuggestedPhoto.tsx @@ -5,7 +5,8 @@ import type { ApiMessageActionSuggestProfilePhoto } from '../../../../api/types/ import { type ApiMessage, type ApiPeer, MAIN_THREAD_ID } from '../../../../api/types'; import { MediaViewerOrigin, SettingsScreens } from '../../../../types'; -import { getPeerTitle, getPhotoMediaHash, getVideoProfilePhotoMediaHash } from '../../../../global/helpers'; +import { getPhotoMediaHash, getVideoProfilePhotoMediaHash } from '../../../../global/helpers'; +import { getPeerTitle } from '../../../../global/helpers/peers'; import { selectPeer } from '../../../../global/selectors'; import { fetchBlob } from '../../../../util/files'; import { renderPeerLink } from '../helpers/messageActions'; diff --git a/src/components/middle/panes/AudioPlayer.tsx b/src/components/middle/panes/AudioPlayer.tsx index 08014ffc9..be157e399 100644 --- a/src/components/middle/panes/AudioPlayer.tsx +++ b/src/components/middle/panes/AudioPlayer.tsx @@ -10,8 +10,9 @@ import type { IconName } from '../../../types/icons'; import { PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION } from '../../../config'; import { - getMediaDuration, getMessageContent, getMessageMediaHash, getPeerTitle, isMessageLocal, + getMediaDuration, getMessageContent, getMessageMediaHash, isMessageLocal, } from '../../../global/helpers'; +import { getPeerTitle } from '../../../global/helpers/peers'; import { selectChat, selectChatMessage, selectSender, selectTabState, } from '../../../global/selectors'; diff --git a/src/components/middle/panes/HeaderPinnedMessage.tsx b/src/components/middle/panes/HeaderPinnedMessage.tsx index 499b6a825..acd44e5d3 100644 --- a/src/components/middle/panes/HeaderPinnedMessage.tsx +++ b/src/components/middle/panes/HeaderPinnedMessage.tsx @@ -12,8 +12,8 @@ import { getMessageMediaHash, getMessageSingleInlineButton, getMessageVideo, - getPeerTitle, } from '../../../global/helpers'; +import { getPeerTitle } from '../../../global/helpers/peers'; import { selectAllowedMessageActionsSlow, selectChat, diff --git a/src/components/middle/panes/PaidMessageChargePane.module.scss b/src/components/middle/panes/PaidMessageChargePane.module.scss index baf0facd2..2f4c69731 100644 --- a/src/components/middle/panes/PaidMessageChargePane.module.scss +++ b/src/components/middle/panes/PaidMessageChargePane.module.scss @@ -33,9 +33,3 @@ 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; -} diff --git a/src/components/middle/panes/PaidMessageChargePane.tsx b/src/components/middle/panes/PaidMessageChargePane.tsx index ad751967e..d7cccdf6f 100644 --- a/src/components/middle/panes/PaidMessageChargePane.tsx +++ b/src/components/middle/panes/PaidMessageChargePane.tsx @@ -7,23 +7,20 @@ import type { } from '../../../api/types'; import { - getPeerTitle, } from '../../../global/helpers'; +import { getPeerTitle } from '../../../global/helpers/peers'; 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'; @@ -41,16 +38,14 @@ type StateProps = { const PaidMessageChargePane: FC = ({ chargedPaidMessageStars, chat, - onPaneStateChange, peerId, + onPaneStateChange, }) => { const isOpen = Boolean(chargedPaidMessageStars); const lang = useLang(); - const [isRemoveFeeDialogOpen, openRemoveFeeDialog, closeRemoveFeeDialog] = useFlag(); - const [shouldRefoundStars, setShouldRefoundStars] = useFlag(false); const { - addNoPaidMessagesException, + openChatRefundModal, } = getActions(); const { ref, shouldRender } = useHeaderPane({ @@ -58,12 +53,8 @@ const PaidMessageChargePane: FC = ({ onStateChange: onPaneStateChange, }); - const handleRemoveFee = useLastCallback(() => { - openRemoveFeeDialog(); - }); - - const handleConfirmRemoveFee = useLastCallback(() => { - addNoPaidMessagesException({ userId: peerId, shouldRefundCharged: shouldRefoundStars }); + const handleRefund = useLastCallback(() => { + openChatRefundModal({ userId: peerId }); }); if (!shouldRender || !chargedPaidMessageStars) return undefined; @@ -80,20 +71,6 @@ const PaidMessageChargePane: FC = ({ withNodes: true, }); - const dialogMessage = lang('ConfirmDialogMessageRemoveFee', { - peer: peerName, - }, { - withMarkdown: true, - withNodes: true, - }); - - const checkBoxTitle = lang('ConfirmDialogRemoveFeeRefundStars', { - amount: chargedPaidMessageStars, - }, { - withMarkdown: true, - withNodes: true, - }); - return (
@@ -106,26 +83,10 @@ const PaidMessageChargePane: FC = ({ fluid size="tiny" className={styles.button} - onClick={handleRemoveFee} + onClick={handleRefund} > {lang('RemoveFeeTitle')} - - - {dialogMessage} - -
); }; diff --git a/src/components/middle/search/MiddleSearchResult.tsx b/src/components/middle/search/MiddleSearchResult.tsx index 4575f52d2..da60cbf1f 100644 --- a/src/components/middle/search/MiddleSearchResult.tsx +++ b/src/components/middle/search/MiddleSearchResult.tsx @@ -2,7 +2,7 @@ import React, { memo } from '../../../lib/teact/teact'; import type { ApiChat, ApiMessage, ApiPeer } from '../../../api/types'; -import { getMessageSenderName } from '../../../global/helpers'; +import { getMessageSenderName } from '../../../global/helpers/peers'; import buildClassName from '../../../util/buildClassName'; import renderText from '../../common/helpers/renderText'; diff --git a/src/components/modals/ModalContainer.tsx b/src/components/modals/ModalContainer.tsx index bd5c7ec71..896deb95a 100644 --- a/src/components/modals/ModalContainer.tsx +++ b/src/components/modals/ModalContainer.tsx @@ -32,6 +32,7 @@ import PreparedMessageModal from './preparedMessage/PreparedMessageModal.async'; import ReportAdModal from './reportAd/ReportAdModal.async'; import ReportModal from './reportModal/ReportModal.async'; import SharePreparedMessageModal from './sharePreparedMessage/SharePreparedMessageModal.async'; +import ChatRefundModal from './stars/chatRefund/ChatRefundModal.async'; import StarsGiftModal from './stars/gift/StarsGiftModal.async'; import StarsBalanceModal from './stars/StarsBalanceModal.async'; import StarsPaymentModal from './stars/StarsPaymentModal.async'; @@ -77,7 +78,8 @@ type ModalKey = keyof Pick; type StateProps = { @@ -127,6 +129,7 @@ const MODALS: ModalRegistry = { preparedMessageModal: PreparedMessageModal, sharePreparedMessageModal: SharePreparedMessageModal, giftTransferModal: GiftTransferModal, + chatRefundModal: ChatRefundModal, }; const MODAL_KEYS = Object.keys(MODALS) as ModalKey[]; const MODAL_ENTRIES = Object.entries(MODALS) as Entries; diff --git a/src/components/modals/gift/GiftComposer.tsx b/src/components/modals/gift/GiftComposer.tsx index 50ffb4536..602dd0e67 100644 --- a/src/components/modals/gift/GiftComposer.tsx +++ b/src/components/modals/gift/GiftComposer.tsx @@ -11,9 +11,8 @@ import { } from '../../../api/types'; import { - getPeerTitle, } from '../../../global/helpers'; -import { isApiPeerUser } from '../../../global/helpers/peers'; +import { getPeerTitle, isApiPeerUser } from '../../../global/helpers/peers'; import { selectPeer, selectPeerPaidMessagesStars, selectTabState, selectTheme, diff --git a/src/components/modals/gift/GiftModal.tsx b/src/components/modals/gift/GiftModal.tsx index a6a5f4ca0..1e0081a8d 100644 --- a/src/components/modals/gift/GiftModal.tsx +++ b/src/components/modals/gift/GiftModal.tsx @@ -14,8 +14,8 @@ import type { TabState } from '../../../global/types'; import type { StarGiftCategory } from '../../../types'; import { STARS_CURRENCY_CODE } from '../../../config'; -import { getPeerTitle, getUserFullName } from '../../../global/helpers'; -import { isApiPeerChat, isApiPeerUser } from '../../../global/helpers/peers'; +import { getUserFullName } from '../../../global/helpers'; +import { getPeerTitle, isApiPeerChat, isApiPeerUser } from '../../../global/helpers/peers'; import { selectPeer } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import { throttle } from '../../../util/schedulers'; diff --git a/src/components/modals/gift/info/GiftInfoModal.tsx b/src/components/modals/gift/info/GiftInfoModal.tsx index 099d6e7c5..783cc8d87 100644 --- a/src/components/modals/gift/info/GiftInfoModal.tsx +++ b/src/components/modals/gift/info/GiftInfoModal.tsx @@ -8,8 +8,8 @@ import type { } from '../../../../api/types'; import type { TabState } from '../../../../global/types'; -import { getHasAdminRight, getPeerTitle } from '../../../../global/helpers'; -import { isApiPeerChat } from '../../../../global/helpers/peers'; +import { getHasAdminRight } from '../../../../global/helpers'; +import { getPeerTitle, isApiPeerChat } from '../../../../global/helpers/peers'; import { selectPeer, selectUser } from '../../../../global/selectors'; import buildClassName from '../../../../util/buildClassName'; import { copyTextToClipboard } from '../../../../util/clipboard'; diff --git a/src/components/modals/gift/transfer/GiftTransferModal.tsx b/src/components/modals/gift/transfer/GiftTransferModal.tsx index 479dd5b45..e3ecf5fd3 100644 --- a/src/components/modals/gift/transfer/GiftTransferModal.tsx +++ b/src/components/modals/gift/transfer/GiftTransferModal.tsx @@ -8,7 +8,7 @@ import type { TabState } from '../../../../global/types'; import type { UniqueCustomPeer } from '../../../../types'; import { ALL_FOLDER_ID } from '../../../../config'; -import { getPeerTitle } from '../../../../global/helpers'; +import { getPeerTitle } from '../../../../global/helpers/peers'; import { selectCanGift, selectPeer } from '../../../../global/selectors'; import { unique } from '../../../../util/iteratees'; import { formatStarsAsIcon, formatStarsAsText } from '../../../../util/localization/format'; diff --git a/src/components/modals/gift/upgrade/GiftUpgradeModal.tsx b/src/components/modals/gift/upgrade/GiftUpgradeModal.tsx index cdf0ba606..cfa944c95 100644 --- a/src/components/modals/gift/upgrade/GiftUpgradeModal.tsx +++ b/src/components/modals/gift/upgrade/GiftUpgradeModal.tsx @@ -14,7 +14,8 @@ import type { import type { TabState } from '../../../../global/types'; import { ApiMediaFormat } from '../../../../api/types'; -import { getPeerTitle, getStickerMediaHash } from '../../../../global/helpers'; +import { getStickerMediaHash } from '../../../../global/helpers'; +import { getPeerTitle } from '../../../../global/helpers/peers'; import { selectPeer } from '../../../../global/selectors'; import { formatStarsAsIcon } from '../../../../util/localization/format'; import { fetch } from '../../../../util/mediaLoader'; diff --git a/src/components/modals/paidReaction/PaidReactionModal.tsx b/src/components/modals/paidReaction/PaidReactionModal.tsx index 57070a2b6..aa3b1c901 100644 --- a/src/components/modals/paidReaction/PaidReactionModal.tsx +++ b/src/components/modals/paidReaction/PaidReactionModal.tsx @@ -14,8 +14,7 @@ import type { TabState } from '../../../global/types'; import type { CustomPeer } from '../../../types'; import { STARS_ICON_PLACEHOLDER } from '../../../config'; -import { getPeerTitle } from '../../../global/helpers'; -import { isApiPeerUser } from '../../../global/helpers/peers'; +import { getPeerTitle, isApiPeerUser } from '../../../global/helpers/peers'; import { selectChat, selectChatMessage, selectPeer, selectUser, } from '../../../global/selectors'; diff --git a/src/components/modals/sharePreparedMessage/SharePreparedMessageModal.tsx b/src/components/modals/sharePreparedMessage/SharePreparedMessageModal.tsx index f2828eaed..2f8d30fc2 100644 --- a/src/components/modals/sharePreparedMessage/SharePreparedMessageModal.tsx +++ b/src/components/modals/sharePreparedMessage/SharePreparedMessageModal.tsx @@ -11,8 +11,8 @@ import type { ThreadId } from '../../../types'; import { MAIN_THREAD_ID } from '../../../api/types'; import { - getPeerTitle, } from '../../../global/helpers'; +import { getPeerTitle } from '../../../global/helpers/peers'; import { selectPeer, selectTabState, } from '../../../global/selectors'; @@ -31,6 +31,8 @@ export type OwnProps = { type StateProps = { isPaymentMessageConfirmDialogOpen: boolean; + starsBalance: number; + isStarsBalanceModalOpen: boolean; }; export type SendParams = { @@ -39,7 +41,7 @@ export type SendParams = { }; const SharePreparedMessageModal: FC = ({ - modal, isPaymentMessageConfirmDialogOpen, + modal, isPaymentMessageConfirmDialogOpen, isStarsBalanceModalOpen, starsBalance, }) => { const { closeSharePreparedMessageModal, @@ -73,7 +75,7 @@ const SharePreparedMessageModal: FC = ({ shouldAutoApprove: shouldPaidMessageAutoApprove, setAutoApprove: setShouldPaidMessageAutoApprove, handleWithConfirmation: handleActionWithPaymentConfirmation, - } = usePaidMessageConfirmation(starsForSendMessage || 0); + } = usePaidMessageConfirmation(starsForSendMessage || 0, isStarsBalanceModalOpen, starsBalance); const handleClose = useLastCallback(() => { closeSharePreparedMessageModal(); @@ -118,7 +120,7 @@ const SharePreparedMessageModal: FC = ({ updateSharePreparedMessageModalSendArgs({ args: { peerId: id, threadId } }); }); - const handleSendWithPaymentConformation = useLastCallback(() => { + const handleSendWithPaymentConfirmation = useLastCallback(() => { if (pendingSendArgs) { handleActionWithPaymentConfirmation(handleSend, pendingSendArgs.peerId, pendingSendArgs.threadId); } @@ -131,7 +133,7 @@ const SharePreparedMessageModal: FC = ({ useEffect(() => { if (pendingSendArgs) { - handleSendWithPaymentConformation(); + handleSendWithPaymentConfirmation(); } }, [pendingSendArgs]); @@ -172,8 +174,12 @@ export default memo(withGlobal( (global): StateProps => { const tabState = selectTabState(global); const { isPaymentMessageConfirmDialogOpen } = tabState; + const starsBalance = global.stars?.balance.amount || 0; + const isStarsBalanceModalOpen = Boolean(tabState.starsBalanceModal); return { isPaymentMessageConfirmDialogOpen, + starsBalance, + isStarsBalanceModalOpen, }; }, )(SharePreparedMessageModal)); diff --git a/src/components/modals/stars/StarsBalanceModal.tsx b/src/components/modals/stars/StarsBalanceModal.tsx index 54f23f414..2fd314f9c 100644 --- a/src/components/modals/stars/StarsBalanceModal.tsx +++ b/src/components/modals/stars/StarsBalanceModal.tsx @@ -8,7 +8,8 @@ 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 { getChatTitle, getUserFullName } from '../../../global/helpers'; +import { getPeerTitle } from '../../../global/helpers/peers'; import { selectChat, selectIsPremiumPurchaseBlocked, selectUser } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import renderText from '../../common/helpers/renderText'; diff --git a/src/components/modals/stars/chatRefund/ChatRefundModal.async.tsx b/src/components/modals/stars/chatRefund/ChatRefundModal.async.tsx new file mode 100644 index 000000000..8e64d43ed --- /dev/null +++ b/src/components/modals/stars/chatRefund/ChatRefundModal.async.tsx @@ -0,0 +1,18 @@ +import type { FC } from '../../../../lib/teact/teact'; +import React from '../../../../lib/teact/teact'; + +import type { OwnProps } from './ChatRefundModal'; + +import { Bundles } from '../../../../util/moduleLoader'; + +import useModuleLoader from '../../../../hooks/useModuleLoader'; + +const ChatRefundModalAsync: FC = (props) => { + const { modal } = props; + const ChatRefundModal = useModuleLoader(Bundles.Stars, 'ChatRefundModal', !modal); + + // eslint-disable-next-line react/jsx-props-no-spreading + return ChatRefundModal ? : undefined; +}; + +export default ChatRefundModalAsync; diff --git a/src/components/modals/stars/chatRefund/ChatRefundModal.tsx b/src/components/modals/stars/chatRefund/ChatRefundModal.tsx new file mode 100644 index 000000000..0293a1deb --- /dev/null +++ b/src/components/modals/stars/chatRefund/ChatRefundModal.tsx @@ -0,0 +1,84 @@ +import React, { memo, useState } from '../../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../../global'; + +import type { ApiUser } from '../../../../api/types'; +import type { TabState } from '../../../../global/types'; + +import { getPeerTitle } from '../../../../global/helpers/peers'; +import { selectUser } from '../../../../global/selectors'; + +import useCurrentOrPrev from '../../../../hooks/useCurrentOrPrev'; +import useLang from '../../../../hooks/useLang'; +import useLastCallback from '../../../../hooks/useLastCallback'; + +import Checkbox from '../../../ui/Checkbox'; +import ConfirmDialog from '../../../ui/ConfirmDialog'; + +export type OwnProps = { + modal: TabState['chatRefundModal']; +}; + +type StateProps = { + user?: ApiUser; +}; + +const ChatRefundModal = ({ modal, user }: OwnProps & StateProps) => { + const { closeChatRefundModal, addNoPaidMessagesException } = getActions(); + + const [shouldRefundStars, setShouldRefundStars] = useState(false); + + const renderingModal = useCurrentOrPrev(modal); + const renderingUser = useCurrentOrPrev(user); + + const { starsToRefund, userId } = renderingModal || {}; + + const lang = useLang(); + + const isOpen = Boolean(modal); + + const handleConfirmRemoveFee = useLastCallback(() => { + closeChatRefundModal(); + if (!userId) return; + addNoPaidMessagesException({ userId, shouldRefundCharged: shouldRefundStars }); + }); + + return ( + + {lang('ConfirmDialogMessageRemoveFee', { + peer: renderingUser && getPeerTitle(lang, renderingUser), + }, { + withMarkdown: true, + withNodes: true, + })} + { + Boolean(starsToRefund) && ( + + ) + } + + ); +}; + +export default memo(withGlobal((global, { modal }): StateProps => { + const user = modal?.userId ? selectUser(global, modal.userId) : undefined; + + return { + user, + }; +})(ChatRefundModal)); diff --git a/src/components/modals/stars/gift/StarsGiftModal.tsx b/src/components/modals/stars/gift/StarsGiftModal.tsx index 3f16bd8d7..78b6ed30a 100644 --- a/src/components/modals/stars/gift/StarsGiftModal.tsx +++ b/src/components/modals/stars/gift/StarsGiftModal.tsx @@ -3,14 +3,14 @@ import React, { memo, useEffect, useMemo, useRef, useState, } from '../../../../lib/teact/teact'; -import { getActions, getGlobal, withGlobal } from '../../../../global'; +import { getActions, withGlobal } from '../../../../global'; import type { ApiStarTopupOption, ApiUser, } from '../../../../api/types'; import type { TabState } from '../../../../global/types'; -import { getPeerTitle } from '../../../../global/helpers'; +import { getPeerTitle } from '../../../../global/helpers/peers'; import { selectUser, } from '../../../../global/selectors'; @@ -213,7 +213,7 @@ const StarsGiftModal: FC = ({ }; export default memo(withGlobal((global, { modal }): StateProps => { - const user = modal?.forUserId ? selectUser(getGlobal(), modal.forUserId) : undefined; + const user = modal?.forUserId ? selectUser(global, modal.forUserId) : undefined; return { user, diff --git a/src/components/modals/stars/subscription/StarsSubscriptionItem.tsx b/src/components/modals/stars/subscription/StarsSubscriptionItem.tsx index 9999a6e2f..ddccd68ae 100644 --- a/src/components/modals/stars/subscription/StarsSubscriptionItem.tsx +++ b/src/components/modals/stars/subscription/StarsSubscriptionItem.tsx @@ -6,7 +6,7 @@ import type { } from '../../../../api/types'; import type { GlobalState } from '../../../../global/types'; -import { getPeerTitle } from '../../../../global/helpers'; +import { getPeerTitle } from '../../../../global/helpers/peers'; import { selectPeer } from '../../../../global/selectors'; import { formatDateToString } from '../../../../util/dates/dateFormat'; import { formatInteger } from '../../../../util/textFormat'; diff --git a/src/components/modals/stars/transaction/StarsTransactionItem.tsx b/src/components/modals/stars/transaction/StarsTransactionItem.tsx index 1571f4ef0..15ce88602 100644 --- a/src/components/modals/stars/transaction/StarsTransactionItem.tsx +++ b/src/components/modals/stars/transaction/StarsTransactionItem.tsx @@ -8,8 +8,8 @@ import type { import type { GlobalState } from '../../../../global/types'; import type { CustomPeer } from '../../../../types'; -import { getPeerTitle } from '../../../../global/helpers'; import { buildStarsTransactionCustomPeer, formatStarsTransactionAmount } from '../../../../global/helpers/payments'; +import { getPeerTitle } from '../../../../global/helpers/peers'; import { selectPeer } from '../../../../global/selectors'; import buildClassName from '../../../../util/buildClassName'; import { formatDateTimeToString } from '../../../../util/dates/dateFormat'; diff --git a/src/components/story/Story.tsx b/src/components/story/Story.tsx index 4fc1e4f92..029452c30 100644 --- a/src/components/story/Story.tsx +++ b/src/components/story/Story.tsx @@ -15,7 +15,8 @@ import type { Signal } from '../../util/signals'; import { MAIN_THREAD_ID } from '../../api/types'; import { EDITABLE_STORY_INPUT_CSS_SELECTOR, EDITABLE_STORY_INPUT_ID } from '../../config'; -import { getPeerTitle, isChatChannel, isUserId } from '../../global/helpers'; +import { isChatChannel, isUserId } from '../../global/helpers'; +import { getPeerTitle } from '../../global/helpers/peers'; import { selectChat, selectIsCurrentUserPremium, diff --git a/src/components/story/StoryPreview.tsx b/src/components/story/StoryPreview.tsx index 025ad6be6..9ac93301e 100644 --- a/src/components/story/StoryPreview.tsx +++ b/src/components/story/StoryPreview.tsx @@ -6,7 +6,8 @@ import type { } from '../../api/types'; import type { StoryViewerOrigin } from '../../types'; -import { getPeerTitle, getStoryMediaHash } from '../../global/helpers'; +import { getStoryMediaHash } from '../../global/helpers'; +import { getPeerTitle } from '../../global/helpers/peers'; import { selectTabState } from '../../global/selectors'; import renderText from '../common/helpers/renderText'; diff --git a/src/components/story/StoryRibbonButton.tsx b/src/components/story/StoryRibbonButton.tsx index a505d6336..19119d2e1 100644 --- a/src/components/story/StoryRibbonButton.tsx +++ b/src/components/story/StoryRibbonButton.tsx @@ -4,7 +4,8 @@ import { getActions } from '../../global'; import type { ApiPeer } from '../../api/types'; import { StoryViewerOrigin } from '../../types'; -import { getPeerTitle, isUserId } from '../../global/helpers'; +import { isUserId } from '../../global/helpers'; +import { getPeerTitle } from '../../global/helpers/peers'; import buildClassName from '../../util/buildClassName'; import { preventMessageInputBlurWithBubbling } from '../middle/helpers/preventMessageInputBlur'; diff --git a/src/components/story/StorySettings.tsx b/src/components/story/StorySettings.tsx index d326ca9a2..7317ddf39 100644 --- a/src/components/story/StorySettings.tsx +++ b/src/components/story/StorySettings.tsx @@ -8,7 +8,8 @@ import type { } from '../../api/types'; import type { IconName } from '../../types/icons'; -import { getPeerTitle, getUserFullName } from '../../global/helpers'; +import { getUserFullName } from '../../global/helpers'; +import { getPeerTitle } from '../../global/helpers/peers'; import { selectPeerStory, selectTabState } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import { getHours } from '../../util/dates/units'; diff --git a/src/components/ui/Notification.scss b/src/components/ui/Notification.scss index 2f95e72a7..9803f0f4f 100644 --- a/src/components/ui/Notification.scss +++ b/src/components/ui/Notification.scss @@ -11,6 +11,8 @@ } .Notification { + --color-toast-action: var(--color-primary); + background-color: rgba(32, 32, 32, 0.8); background-size: 1.5rem; border-radius: var(--border-radius-default); diff --git a/src/components/ui/Radio.scss b/src/components/ui/Radio.scss index f3013e7ea..4e5c376b2 100644 --- a/src/components/ui/Radio.scss +++ b/src/components/ui/Radio.scss @@ -54,6 +54,14 @@ } } + &.canCheckedInDisabled { + .Radio-main { + &::before { + visibility: visible; + } + } + } + > input { position: absolute; z-index: var(--z-below); diff --git a/src/components/ui/Radio.tsx b/src/components/ui/Radio.tsx index 7ac69d046..0a966f7f1 100644 --- a/src/components/ui/Radio.tsx +++ b/src/components/ui/Radio.tsx @@ -18,6 +18,7 @@ type OwnProps = { value?: string; checked?: boolean; disabled?: boolean; + isCanCheckedInDisabled?: boolean; isLink?: boolean; hidden?: boolean; isLoading?: boolean; @@ -46,6 +47,7 @@ const Radio: FC = ({ isLink, onChange, onSubLabelClick, + isCanCheckedInDisabled, }) => { const lang = useOldLang(); @@ -58,6 +60,7 @@ const Radio: FC = ({ isLoading && 'loading', onlyInput && 'onlyInput', Boolean(subLabel) && 'withSubLabel', + isCanCheckedInDisabled && 'canCheckedInDisabled', ); return ( diff --git a/src/components/ui/RadioGroup.tsx b/src/components/ui/RadioGroup.tsx index 9e17641bd..816a097e3 100644 --- a/src/components/ui/RadioGroup.tsx +++ b/src/components/ui/RadioGroup.tsx @@ -14,6 +14,7 @@ export type IRadioOption = { value: T; hidden?: boolean; className?: string; + isCanCheckedInDisabled?: boolean; }; type OwnProps = { @@ -67,6 +68,7 @@ const RadioGroup: FC = ({ value={option.value} checked={option.value === selected} hidden={option.hidden} + isCanCheckedInDisabled={option.isCanCheckedInDisabled} disabled={disabled} withIcon={withIcon} isLoading={loadingOption ? loadingOption === option.value : undefined} diff --git a/src/global/actions/api/bots.ts b/src/global/actions/api/bots.ts index ef29230d3..333ca9923 100644 --- a/src/global/actions/api/bots.ts +++ b/src/global/actions/api/bots.ts @@ -416,7 +416,7 @@ addActionHandler('sendInlineBotResult', async (global, actions, payload): Promis actions.resetDraftReplyInfo({ tabId }); actions.clearWebPagePreview({ tabId }); - const starsForOneMessage = await getPeerStarsForMessage(global, chat); + const starsForOneMessage = await getPeerStarsForMessage(global, chatId); const params = { chat, id, diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 1c50674f5..00a802e93 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -8,7 +8,6 @@ import type { ApiInputStoryReplyInfo, ApiMessage, ApiOnProgress, - ApiPeer, ApiStory, ApiUser, } from '../../../api/types'; @@ -61,12 +60,11 @@ import { isChatSuperGroup, isDeletedUser, isMessageLocal, - isPeerUser, isServiceNotificationMessage, isUserBot, splitMessagesForForwarding, } from '../../helpers'; -import { isApiPeerUser } from '../../helpers/peers'; +import { isApiPeerChat, isApiPeerUser } from '../../helpers/peers'; import { addActionHandler, getActions, getGlobal, setGlobal, } from '../../index'; @@ -345,7 +343,7 @@ addActionHandler('sendMessage', async (global, actions, payload): Promise const replyInfo = storyReplyInfo || messageReplyInfo; const lastMessageId = selectChatLastMessageId(global, chatId!); - const messagePriceInStars = await getPeerStarsForMessage(global, chat); + const messagePriceInStars = await getPeerStarsForMessage(global, chatId!); const params : SendMessageParams = { ...payload, @@ -1607,9 +1605,12 @@ function getViewportSlice( export async function getPeerStarsForMessage( global: T, - peer: ApiPeer, + peerId: string, ): Promise { - if (!isPeerUser(peer)) { + const peer = selectPeer(global, peerId); + if (!peer) return undefined; + + if (isApiPeerChat(peer)) { return peer.paidMessagesStars; } @@ -1617,7 +1618,7 @@ export async function getPeerStarsForMessage( const fullInfo = selectUserFullInfo(global, peer.id); if (fullInfo) { - return fullInfo?.paidMessagesStars; + return fullInfo.paidMessagesStars; } const result = await callApi('fetchPaidMessagesStarsAmount', peer); @@ -1675,7 +1676,7 @@ async function sendMessagesWithNotification( ) { const chat = sendParams[0]?.chat; if (!chat || !sendParams.length) return; - const starsForOneMessage = await getPeerStarsForMessage(global, chat); + const starsForOneMessage = await getPeerStarsForMessage(global, chat.id); if (!starsForOneMessage) { // eslint-disable-next-line eslint-multitab-tt/no-getactions-in-actions getActions().sendMessages({ sendParams }); diff --git a/src/global/actions/api/users.ts b/src/global/actions/api/users.ts index 7d59d8d03..2766b0434 100644 --- a/src/global/actions/api/users.ts +++ b/src/global/actions/api/users.ts @@ -204,6 +204,27 @@ addActionHandler('addNoPaidMessagesException', async (global, actions, payload): setGlobal(global); }); +addActionHandler('openChatRefundModal', async (global, actions, payload): Promise => { + const { userId, tabId = getCurrentTabId() } = payload; + const user = selectUser(global, userId); + if (!user) { + return; + } + + const starsAmount = await callApi('fetchPaidMessagesRevenue', { user }); + if (starsAmount === undefined) return; + + global = getGlobal(); + global = updateTabState(global, { + chatRefundModal: { + userId, + starsToRefund: starsAmount, + }, + }, tabId); + + setGlobal(global); +}); + addActionHandler('updateContact', async (global, actions, payload): Promise => { const { userId, isMuted = false, firstName, lastName, shouldSharePhoneNumber, diff --git a/src/global/actions/apiUpdaters/misc.ts b/src/global/actions/apiUpdaters/misc.ts index f740fc344..42e6a5b04 100644 --- a/src/global/actions/apiUpdaters/misc.ts +++ b/src/global/actions/apiUpdaters/misc.ts @@ -3,7 +3,7 @@ import { PaymentStep } from '../../../types'; import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config'; import { applyLangPackDifference, getTranslationFn, requestLangPackDifference } from '../../../util/localization'; -import { getPeerTitle } from '../../helpers'; +import { getPeerTitle } from '../../helpers/peers'; import { addActionHandler, setGlobal } from '../../index'; import { addBlockedUser, diff --git a/src/global/actions/ui/chats.ts b/src/global/actions/ui/chats.ts index 5e2e13469..2c2c35df5 100644 --- a/src/global/actions/ui/chats.ts +++ b/src/global/actions/ui/chats.ts @@ -40,7 +40,6 @@ addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionRe actions.closeStoryViewer({ tabId }); actions.closeStarsBalanceModal({ tabId }); - actions.closeStarsBalanceModal({ tabId }); actions.closeStarsTransactionModal({ tabId }); if (!currentMessageList || ( diff --git a/src/global/actions/ui/messages.ts b/src/global/actions/ui/messages.ts index e4913aaa7..14b7aa7b2 100644 --- a/src/global/actions/ui/messages.ts +++ b/src/global/actions/ui/messages.ts @@ -29,10 +29,10 @@ import { getMediaHash, getMessageDownloadableMedia, getMessageStatefulContent, - getPeerTitle, isChatChannel, } from '../../helpers'; import { getMessageSummaryText } from '../../helpers/messageSummary'; +import { getPeerTitle } from '../../helpers/peers'; import { renderMessageSummaryHtml } from '../../helpers/renderMessageSummaryHtml'; import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { @@ -62,7 +62,6 @@ import { selectIsRightColumnShown, selectIsViewportNewest, selectMessageIdsByGroupId, - selectPeer, selectPinnedIds, selectReplyStack, selectRequestedChatTranslationLanguage, @@ -1115,8 +1114,7 @@ addActionHandler('updateSharePreparedMessageModalSendArgs', async (global, actio return; } - const peer = selectPeer(global, args.peerId); - const starsForSendMessage = peer ? await getPeerStarsForMessage(global, peer) : undefined; + const starsForSendMessage = await getPeerStarsForMessage(global, args.peerId); global = getGlobal(); global = updateTabState(global, { diff --git a/src/global/actions/ui/misc.ts b/src/global/actions/ui/misc.ts index 104a55a47..944b266ed 100644 --- a/src/global/actions/ui/misc.ts +++ b/src/global/actions/ui/misc.ts @@ -32,6 +32,7 @@ import { selectCurrentMessageList, selectIsCurrentUserPremium, selectIsTrustedBot, + selectPeerPaidMessagesStars, selectSender, selectTabState, selectTopic, @@ -325,7 +326,19 @@ addActionHandler('showNotification', (global, actions, payload): ActionReturnTyp }); addActionHandler('showAllowedMessageTypesNotification', (global, actions, payload): ActionReturnType => { - const { chatId, tabId = getCurrentTabId() } = payload; + const { chatId, messageListType, tabId = getCurrentTabId() } = payload; + + const paidMessagesStars = selectPeerPaidMessagesStars(global, chatId); + + if (paidMessagesStars && messageListType === 'scheduled') { + actions.showNotification({ + message: { + key: 'DescriptionScheduledPaidMessagesNotAllowed', + }, + tabId, + }); + return; + } const chat = selectChat(global, chatId); if (!chat) return; diff --git a/src/global/actions/ui/stories.ts b/src/global/actions/ui/stories.ts index d4822c0cd..0b6284b75 100644 --- a/src/global/actions/ui/stories.ts +++ b/src/global/actions/ui/stories.ts @@ -9,7 +9,6 @@ import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { addStoriesForPeer } from '../../reducers'; import { updateTabState } from '../../reducers/tabs'; import { - selectChat, selectCurrentViewedStory, selectPeer, selectPeerFirstStoryId, @@ -296,13 +295,11 @@ addActionHandler('sendMessage', async (global, actions, payload): Promise const { storyId, peerId: storyPeerId } = selectCurrentViewedStory(global, tabId); const isStoryReply = Boolean(storyId && storyPeerId); - const chat = storyPeerId ? selectChat(global, storyPeerId) : undefined; - if (!chat) return; - const messagePriceInStars = await getPeerStarsForMessage(global, chat); - - if (!isStoryReply || messagePriceInStars) { + if (!isStoryReply) { return; } + const messagePriceInStars = await getPeerStarsForMessage(global, storyPeerId!); + if (messagePriceInStars === undefined) return; const { gif, sticker, isReaction } = payload; diff --git a/src/global/actions/ui/users.ts b/src/global/actions/ui/users.ts index d928a7652..7af3f6d93 100644 --- a/src/global/actions/ui/users.ts +++ b/src/global/actions/ui/users.ts @@ -1,6 +1,7 @@ import type { ActionReturnType } from '../../types'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; +import { addTabStateResetterAction } from '../../helpers/meta'; import { addActionHandler } from '../../index'; import { closeNewContactDialog, updateUserSearch } from '../../reducers'; import { updateTabState } from '../../reducers/tabs'; @@ -50,3 +51,5 @@ addActionHandler('closeSuggestedStatusModal', (global, actions, payload): Action suggestedStatusModal: undefined, }, tabId); }); + +addTabStateResetterAction('closeChatRefundModal', 'chatRefundModal'); diff --git a/src/global/helpers/chats.ts b/src/global/helpers/chats.ts index 397605e91..959b10368 100644 --- a/src/global/helpers/chats.ts +++ b/src/global/helpers/chats.ts @@ -9,7 +9,6 @@ import type { ApiPeer, ApiPreparedInlineMessage, ApiTopic, - ApiUser, } from '../../api/types'; import type { OldLangFn } from '../../hooks/useOldLang'; import type { @@ -27,7 +26,7 @@ import { formatDateToString, formatTime } from '../../util/dates/dateFormat'; import { getServerTime } from '../../util/serverTime'; import { getGlobal } from '..'; import { isSystemBot } from './bots'; -import { getMainUsername, getUserFirstOrLastName } from './users'; +import { getMainUsername } from './users'; const FOREVER_BANNED_DATE = Date.now() / 1000 + 31622400; // 366 days @@ -35,14 +34,6 @@ export function isUserId(entityId: string) { return !entityId.startsWith('-'); } -export function isPeerChat(entity: ApiPeer): entity is ApiChat { - return 'title' in entity; -} - -export function isPeerUser(entity: ApiPeer): entity is ApiUser { - return !isPeerChat(entity); -} - export function isChannelId(entityId: string) { return entityId.length === CHANNEL_ID_LENGTH && entityId.startsWith('-1'); } @@ -211,8 +202,10 @@ export function getAllowedAttachmentOptions( chatFullInfo?: ApiChatFullInfo, isChatWithBot = false, isStoryReply = false, + paidMessagesStars?: number, + isInScheduledList = false, ): IAllowedAttachmentOptions { - if (!chat) { + if (!chat || (paidMessagesStars && isInScheduledList)) { return { canAttachMedia: false, canAttachPolls: false, @@ -345,24 +338,6 @@ export function getFolderDescriptionText(lang: OldLangFn, folder: ApiChatFolder, } } -export function getMessageSenderName(lang: OldLangFn, chatId: string, sender?: ApiPeer) { - if (!sender || isUserId(chatId)) { - return undefined; - } - - if (isPeerChat(sender)) { - if (chatId === sender.id) return undefined; - - return sender.title; - } - - if (sender.isSelf) { - return lang('FromYou'); - } - - return getUserFirstOrLastName(sender); -} - export function isChatPublic(chat: ApiChat) { return chat.usernames?.some(({ isActive }) => isActive); } diff --git a/src/global/helpers/messages.ts b/src/global/helpers/messages.ts index d9ea6cb58..b14a469e8 100644 --- a/src/global/helpers/messages.ts +++ b/src/global/helpers/messages.ts @@ -9,9 +9,7 @@ import type { import type { ApiPoll, MediaContainer, StatefulMediaContent, } from '../../api/types/messages'; -import type { OldLangFn } from '../../hooks/useOldLang'; -import type { CustomPeer, ThreadId } from '../../types'; -import type { LangFn } from '../../util/localization'; +import type { ThreadId } from '../../types'; import type { GlobalState } from '../types'; import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types'; @@ -32,9 +30,9 @@ import { isLocalMessageId } from '../../util/keys/messageKey'; import { getServerTime } from '../../util/serverTime'; import { getGlobal } from '../index'; import { - getChatTitle, getCleanPeerId, isPeerUser, isUserId, + getCleanPeerId, isUserId, } from './chats'; -import { getMainUsername, getUserFirstOrLastName, getUserFullName } from './users'; +import { getMainUsername } from './users'; const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i'); @@ -209,24 +207,6 @@ export function isAnonymousOwnMessage(message: ApiMessage) { return Boolean(message.senderId) && !isUserId(message.senderId) && isOwnMessage(message); } -export function getPeerTitle(lang: OldLangFn | LangFn, peer: ApiPeer | CustomPeer) { - if (!peer) return undefined; - if ('isCustomPeer' in peer) { - // TODO: Remove any after full migration to new lang - return peer.titleKey ? lang(peer.titleKey as any) : peer.title; - } - return isPeerUser(peer) ? getUserFirstOrLastName(peer) : getChatTitle(lang, peer); -} - -export function getPeerFullTitle(lang: OldLangFn | LangFn, peer: ApiPeer | CustomPeer) { - if (!peer) return undefined; - if ('isCustomPeer' in peer) { - // TODO: Remove any after full migration to new lang - return peer.titleKey ? lang(peer.titleKey as any) : peer.title; - } - return isPeerUser(peer) ? getUserFullName(peer) : getChatTitle(lang, peer); -} - export function getSendingState(message: ApiMessage) { if (!message.sendingState) { return 'succeeded'; diff --git a/src/global/helpers/peers.ts b/src/global/helpers/peers.ts index 7fff07ddd..7d201b91e 100644 --- a/src/global/helpers/peers.ts +++ b/src/global/helpers/peers.ts @@ -1,12 +1,14 @@ import type { ApiChat, ApiPeer, ApiUser } from '../../api/types'; +import type { OldLangFn } from '../../hooks/useOldLang'; +import type { CustomPeer } from '../../types'; import { SERVICE_NOTIFICATIONS_USER_ID } from '../../config'; -import { getTranslationFn } from '../../util/localization'; +import { getTranslationFn, type LangFn } from '../../util/localization'; import { prepareSearchWordsForNeedle } from '../../util/searchWords'; import { selectChat, selectPeer, selectUser } from '../selectors'; import { getGlobal } from '..'; -import { getChatTitle } from './chats'; -import { getPeerFullTitle } from './messages'; +import { getChatTitle, isUserId } from './chats'; +import { getUserFirstOrLastName, getUserFullName } from './users'; export function isApiPeerChat(peer: ApiPeer): peer is ApiChat { return 'title' in peer; @@ -89,3 +91,39 @@ export function getPeerTypeKey(peer: ApiPeer) { return 'ChatList.PeerTypeNonContactUser'; } + +export function getPeerTitle(lang: OldLangFn | LangFn, peer: ApiPeer | CustomPeer) { + if (!peer) return undefined; + if ('isCustomPeer' in peer) { + // TODO: Remove any after full migration to new lang + return peer.titleKey ? lang(peer.titleKey as any) : peer.title; + } + return isApiPeerUser(peer) ? getUserFirstOrLastName(peer) : getChatTitle(lang, peer); +} + +export function getPeerFullTitle(lang: OldLangFn | LangFn, peer: ApiPeer | CustomPeer) { + if (!peer) return undefined; + if ('isCustomPeer' in peer) { + // TODO: Remove any after full migration to new lang + return peer.titleKey ? lang(peer.titleKey as any) : peer.title; + } + return isApiPeerUser(peer) ? getUserFullName(peer) : getChatTitle(lang, peer); +} + +export function getMessageSenderName(lang: OldLangFn, chatId: string, sender?: ApiPeer) { + if (!sender || isUserId(chatId)) { + return undefined; + } + + if (isApiPeerChat(sender)) { + if (chatId === sender.id) return undefined; + + return sender.title; + } + + if (sender.isSelf) { + return lang('FromYou'); + } + + return getUserFirstOrLastName(sender); +} diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index ee97e683b..4e7097fe7 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -1741,6 +1741,10 @@ export interface ActionPayloads { userId: string; shouldRefundCharged: boolean; }; + openChatRefundModal: { + userId: string; + } & WithTabId; + closeChatRefundModal: WithTabId | undefined; loadMoreProfilePhotos: { peerId: string; isPreload?: boolean; @@ -2186,6 +2190,7 @@ export interface ActionPayloads { showNotification: Omit & { localId?: string } & WithTabId; showAllowedMessageTypesNotification: { chatId: string; + messageListType?: MessageListType; } & WithTabId; dismissNotification: { localId: string } & WithTabId; diff --git a/src/global/types/tabState.ts b/src/global/types/tabState.ts index e9652cb28..874561541 100644 --- a/src/global/types/tabState.ts +++ b/src/global/types/tabState.ts @@ -638,6 +638,10 @@ export type TabState = { forPeerId: string; gifts?: ApiPremiumGiftCodeOption[]; }; + chatRefundModal?: { + userId: string; + starsToRefund: number; + }; limitReachedModal?: { limit: ApiLimitTypeWithModal; diff --git a/src/hooks/useMessageMediaMetadata.ts b/src/hooks/useMessageMediaMetadata.ts index 9940a009f..81f41a467 100644 --- a/src/hooks/useMessageMediaMetadata.ts +++ b/src/hooks/useMessageMediaMetadata.ts @@ -6,8 +6,9 @@ import type { } from '../api/types'; import { - getAudioHasCover, getChatAvatarHash, getChatTitle, getMediaHash, getMessageContent, getPeerTitle, + getAudioHasCover, getChatAvatarHash, getChatTitle, getMediaHash, getMessageContent, } from '../global/helpers'; +import { getPeerTitle } from '../global/helpers/peers'; import { resizeImage, scaleImage } from '../util/imageResize'; import { buildMediaMetadata } from '../util/mediaSession'; import { AVATAR_FULL_DIMENSIONS } from '../components/common/helpers/mediaDimensions'; diff --git a/src/lib/gramjs/tl/apiTl.ts b/src/lib/gramjs/tl/apiTl.ts index bb459d826..7c94e636a 100644 --- a/src/lib/gramjs/tl/apiTl.ts +++ b/src/lib/gramjs/tl/apiTl.ts @@ -1473,6 +1473,7 @@ account.resolveBusinessChatLink#5492e5ee slug:string = account.ResolvedBusinessC 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; +account.getPaidMessagesRevenue#f1266f38 user_id:InputUser = account.PaidMessagesRevenue; users.getUsers#d91a548 id:Vector = Vector; users.getFullUser#b60f5918 id:InputUser = users.UserFull; users.getRequirementsToContact#d89a83a3 id:Vector = Vector; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index e24f69758..30abe2149 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -62,6 +62,7 @@ "account.toggleSponsoredMessages", "account.getCollectibleEmojiStatuses", "account.addNoPaidMessagesException", + "account.getPaidMessagesRevenue", "users.getUsers", "users.getFullUser", "contacts.getContacts", diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index 370c4cdf0..99782270f 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -103,7 +103,6 @@ $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%)}; diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 69294d82f..b9f78bb4d 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1447,6 +1447,9 @@ export interface LangPair { 'StoryTooltipReactionSent': undefined; 'StarsNeededTextSendPaidMessages': undefined; 'PaidMessageTransactionTotal': undefined; + 'DescriptionRestrictedMedia': undefined; + 'DescriptionScheduledPaidMediaNotAllowed': undefined; + 'DescriptionScheduledPaidMessagesNotAllowed': undefined; } export interface LangPairWithVariables { diff --git a/src/util/notifications.tsx b/src/util/notifications.tsx index 6eeb793e3..fab54b3bf 100644 --- a/src/util/notifications.tsx +++ b/src/util/notifications.tsx @@ -12,12 +12,12 @@ import { getChatAvatarHash, getChatTitle, getMessageRecentReaction, - getMessageSenderName, getPrivateChatUserId, getUserFullName, isChatChannel, } from '../global/helpers'; import { getIsChatMuted, getIsChatSilent, getShouldShowMessagePreview } from '../global/helpers/notifications'; +import { getMessageSenderName } from '../global/helpers/peers'; import { selectChat, selectCurrentMessageList,