diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index b6d648807..966fd0c4b 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -368,16 +368,20 @@ export function buildApiFactCheck(factCheck: GramJs.FactCheck): ApiFactCheck { function buildApiMessageActionStarGift(action: GramJs.MessageActionStarGift) : ApiMessageActionStarGift { const { - nameHidden, saved, converted, gift, message, convertStars, + nameHidden, saved, converted, gift, message, convertStars, canUpgrade, upgraded, upgradeMsgId, } = action; return { + type: 'starGift', isNameHidden: Boolean(nameHidden), isSaved: Boolean(saved), - isConverted: Boolean(converted), + isConverted: converted, gift: buildApiStarGift(gift), message: message && buildApiFormattedText(message), starsToConvert: convertStars?.toJSNumber(), + canUpgrade, + isUpgraded: upgraded, + upgradeMsgId, }; } @@ -389,6 +393,7 @@ function buildApiMessageActionStarGiftUnique( } = action; return { + type: 'starGiftUnique', gift: buildApiStarGift(gift), canExportAt, isRefunded: refunded, @@ -747,7 +752,13 @@ function buildAction( currency = STARS_CURRENCY_CODE; } else if (action instanceof GramJs.MessageActionStarGiftUnique && action.gift instanceof GramJs.StarGiftUnique) { type = 'starGiftUnique'; - text = isOutgoing ? 'Notification.StarsGift.UpgradeYou' : 'Notification.StarsGift.Upgrade'; + if (isOutgoing) { + text = action.upgrade ? 'Notification.StarsGift.UpgradeYou' : 'ActionUniqueGiftTransferOutbound'; + } else { + text = action.upgrade ? 'Notification.StarsGift.Upgrade' : 'ActionUniqueGiftTransferInbound'; + translationValues.push('%action_origin%'); + } + starGift = buildApiMessageActionStarGiftUnique(action); if (targetPeerId) { diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 3126213d3..540031573 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -459,15 +459,20 @@ export type ApiNewPoll = { }; export interface ApiMessageActionStarGift { + type: 'starGift'; isNameHidden: boolean; isSaved: boolean; - isConverted?: boolean; + isConverted?: true; gift: ApiStarGift; message?: ApiFormattedText; starsToConvert?: number; + canUpgrade?: true; + isUpgraded?: true; + upgradeMsgId?: number; } export interface ApiMessageActionStarGiftUnique { + type: 'starGiftUnique'; isUpgrade?: true; isTransferred?: true; isSaved?: true; diff --git a/src/api/types/payments.ts b/src/api/types/payments.ts index e575597c4..a40417592 100644 --- a/src/api/types/payments.ts +++ b/src/api/types/payments.ts @@ -265,6 +265,7 @@ export interface ApiUserStarGift { messageId?: number; starsToConvert?: number; isConverted?: boolean; // Local field, used for Action Message + upgradeMsgId?: number; // Local field, used for Action Message } export interface ApiPremiumGiftCodeOption { diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index cb558c087..587f06d7a 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1357,6 +1357,7 @@ "GiftInfoOriginalInfoTextSender" = "Gifted by {sender} to {user} on {date} with comment \"{text}\"." "GiftInfoStatus" = "Status"; "GiftInfoStatusNonUnique" = "Non-Unique"; +"GiftInfoViewUpgraded" = "View Upgraded Gift"; "StarsAmount" = "⭐️{amount}"; "StarsAmountText_one" = "{amount} Star"; "StarsAmountText_other" = "{amount} Stars"; @@ -1377,6 +1378,10 @@ "ActionStarGiftOutDescription" = "{user} can display this gift on their page or convert it to {count} Stars."; "ActionStarGiftDescription" = "Display this gift on your page or convert it to {count} Stars."; "ActionStarGiftDisplaying" = "You kept this gift on your page."; +"ActionStarGiftOutDescriptionUpgrade" = "{user} can turn this gift to a unique collectible."; +"ActionStarGiftDescriptionUpgrade" = "Tap “Unpack” to turn this gift to a unique collectible."; +"ActionStarGiftUpgraded" = "This gift was upgraded."; +"ActionStarGiftUnpack" = "Unpack"; "GiftTo" = "Gift to"; "GiftFrom" = "Gift from"; "ReceivedGift" = "Received Gift"; @@ -1474,5 +1479,6 @@ "ProfileTabVoice" = "Voice"; "ProfileTabSharedGroups" = "Groups"; "ProfileTabSimilarChannels" = "Similar Channels"; +"ActionUnsupportedTitle" = "Action not supported yet"; +"ActionUnsupportedDescription" = "Please, use one of our apps to complete this action."; "LocationPermissionText" = "**{name}** requests access to set your **location**. You will be able to revoke this access in the profile page of **{name}**."; - diff --git a/src/components/common/helpers/renderActionMessageText.tsx b/src/components/common/helpers/renderActionMessageText.tsx index 36f8cdaa7..a48aa6b46 100644 --- a/src/components/common/helpers/renderActionMessageText.tsx +++ b/src/components/common/helpers/renderActionMessageText.tsx @@ -97,6 +97,11 @@ export function renderActionMessageText( unprocessed = unprocessed .replace('%@', '%action_origin%'); } + if (translationKey.startsWith('ActionUniqueGiftTransfer')) { + unprocessed = unprocessed + .replace('un1', '%action_origin%') + .replace(/\*\*/g, ''); + } if (translationKey === 'BoostingReceivedPrizeFrom') { unprocessed = unprocessed .replace('**%s**', '%target_chat%') diff --git a/src/components/common/profile/ChatExtra.tsx b/src/components/common/profile/ChatExtra.tsx index 10aac92b3..19bdd6305 100644 --- a/src/components/common/profile/ChatExtra.tsx +++ b/src/components/common/profile/ChatExtra.tsx @@ -5,12 +5,12 @@ import React, { import { getActions, withGlobal } from '../../../global'; import type { + ApiBotVerification, ApiChat, ApiCountryCode, ApiUser, ApiUserFullInfo, ApiUsername, - ApiBotVerification, } from '../../../api/types'; import type { BotAppPermissions } from '../../../types'; import { MAIN_THREAD_ID } from '../../../api/types'; diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index b21a191d5..9e078fb40 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -42,6 +42,7 @@ import { selectOutgoingStatus, selectPeer, selectPeerStory, + selectSender, selectTabState, selectThreadParam, selectTopicFromMessage, @@ -452,10 +453,11 @@ export default memo(withGlobal( const lastMessage = previewMessageId ? selectChatMessage(global, chatId, previewMessageId) : selectChatLastMessage(global, chatId, isSavedDialog ? 'saved' : 'all'); - const { senderId, isOutgoing, forwardInfo } = lastMessage || {}; - const actualSenderId = isSavedDialog ? forwardInfo?.fromId : senderId; + const { isOutgoing, forwardInfo } = lastMessage || {}; + const savedDialogSender = isSavedDialog && forwardInfo?.fromId ? selectPeer(global, forwardInfo.fromId) : undefined; + const messageSender = lastMessage ? selectSender(global, lastMessage) : undefined; + const lastMessageSender = savedDialogSender || messageSender; const replyToMessageId = lastMessage && getMessageReplyInfo(lastMessage)?.replyToMsgId; - const lastMessageSender = actualSenderId ? selectPeer(global, actualSenderId) : undefined; const lastMessageAction = lastMessage ? getMessageAction(lastMessage) : undefined; const actionTargetMessage = lastMessageAction && replyToMessageId ? selectChatMessage(global, chat.id, replyToMessageId) diff --git a/src/components/left/main/Topic.tsx b/src/components/left/main/Topic.tsx index c467a0a29..10815184a 100644 --- a/src/components/left/main/Topic.tsx +++ b/src/components/left/main/Topic.tsx @@ -20,10 +20,10 @@ import { selectDraft, selectOutgoingStatus, selectPeerStory, + selectSender, selectThreadInfo, selectThreadParam, selectTopics, - selectUser, } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import { createLocationHash } from '../../../util/routing'; @@ -248,10 +248,9 @@ export default memo(withGlobal( const chat = selectChat(global, chatId); const lastMessage = selectChatMessage(global, chatId, topic.lastMessageId); - const { senderId, isOutgoing } = lastMessage || {}; + const { isOutgoing } = lastMessage || {}; const replyToMessageId = lastMessage && getMessageReplyInfo(lastMessage)?.replyToMsgId; - const lastMessageSender = senderId - ? (selectUser(global, senderId) || selectChat(global, senderId)) : undefined; + const lastMessageSender = lastMessage && selectSender(global, lastMessage); const lastMessageAction = lastMessage ? getMessageAction(lastMessage) : undefined; const actionTargetMessage = lastMessageAction && replyToMessageId ? selectChatMessage(global, chatId, replyToMessageId) diff --git a/src/components/left/main/hooks/useChatListEntry.tsx b/src/components/left/main/hooks/useChatListEntry.tsx index a6e43c97e..412ad348d 100644 --- a/src/components/left/main/hooks/useChatListEntry.tsx +++ b/src/components/left/main/hooks/useChatListEntry.tsx @@ -22,6 +22,7 @@ import { getMessageVideo, isActionMessage, isChatChannel, + isChatGroup, isExpiredMessage, } from '../../../../global/helpers'; import { getMessageReplyInfo } from '../../../../global/helpers/replies'; @@ -154,7 +155,7 @@ export default function useChatListEntry({ } if (isAction) { - const isChat = chat && (isChatChannel(chat) || lastMessage.senderId === lastMessage.chatId); + const isChat = chat && (isChatChannel(chat) || isChatGroup(chat)); return (

diff --git a/src/components/middle/ActionMessage.tsx b/src/components/middle/ActionMessage.tsx index 8371abd72..fbf199a53 100644 --- a/src/components/middle/ActionMessage.tsx +++ b/src/components/middle/ActionMessage.tsx @@ -127,6 +127,7 @@ const ActionMessage: FC = ({ getReceipt, openGiftInfoModalFromMessage, openPrizeStarsTransactionFromGiveaway, + showNotification, } = getActions(); const oldLang = useOldLang(); @@ -234,6 +235,19 @@ const ActionMessage: FC = ({ }; const handleStarGiftClick = () => { + const starGift = message.content.action?.starGift; + if (!starGift) return; + if (starGift.type === 'starGift' && starGift.canUpgrade && !message.isOutgoing) { + showNotification({ + title: { + key: 'ActionUnsupportedTitle', + }, + message: { + key: 'ActionUnsupportedDescription', + }, + }); + } + openGiftInfoModalFromMessage({ chatId: message.chatId, messageId: message.id, @@ -402,9 +416,10 @@ const ActionMessage: FC = ({ function renderStarGiftUserCaption() { const targetUser = targetUsers && targetUsers[0]; - if (!targetUser || !senderUser) return undefined; + const starGift = message.content.action?.starGift; + if (!targetUser || !senderUser || !starGift) return undefined; - if (message.isOutgoing) { + if (message.isOutgoing || (starGift.type === 'starGiftUnique' && starGift.isUpgrade)) { return (

{lang('GiftTo')} @@ -432,45 +447,65 @@ const ActionMessage: FC = ({ if (starGiftMessage) { return renderTextWithEntities({ text: starGiftMessage.text, entities: starGiftMessage.entities }); } - const amount = starGift?.starsToConvert; + const amountToConvert = starGift?.starsToConvert; if (message.isOutgoing) { - return lang('ActionStarGiftOutDescription', { - user: targetUser || 'User', - count: amount, - }, { withNodes: true }); + if (amountToConvert) { + return lang('ActionStarGiftOutDescription', { + user: targetUser || 'User', + count: amountToConvert, + }, { withNodes: true }); + } + + if (starGift.canUpgrade) { + return lang('ActionStarGiftOutDescriptionUpgrade', { + user: targetUser || 'User', + }); + } } if (starGift.isSaved) { return lang('ActionStarGiftDisplaying'); } + if (starGift.isUpgraded) { + return lang('ActionStarGiftUpgraded'); + } + if (starGift.isConverted) { return message.isOutgoing ? lang('GiftInfoDescriptionOutConverted', { - amount: formatInteger(amount!), + amount: formatInteger(amountToConvert!), user: targetUser || 'User', }, { - pluralValue: amount!, + pluralValue: amountToConvert!, withNodes: true, withMarkdown: true, }) : lang('GiftInfoDescriptionConverted', { - amount: formatInteger(amount!), + amount: formatInteger(amountToConvert!), }, { - pluralValue: amount!, + pluralValue: amountToConvert!, withNodes: true, withMarkdown: true, }); } - return lang('ActionStarGiftDescription', { - count: amount, - }, { withNodes: true }); + if (amountToConvert) { + return lang('ActionStarGiftDescription', { + count: amountToConvert, + }, { withNodes: true }); + } + + if (starGift.canUpgrade) { + return lang('ActionStarGiftDescriptionUpgrade'); + } + + return undefined; } function renderStarGift() { - const starGift = message.content.action?.starGift; + const starGift = message.content.action?.starGift as ApiMessageActionStarGift; if (!starGift || starGift.gift.type !== 'starGift') return undefined; return ( @@ -494,12 +529,10 @@ const ActionMessage: FC = ({ {renderStarGiftUserDescription()}
- {!message.isOutgoing && ( -
- - {oldLang('ActionGiftPremiumView')} -
- )} +
+ + {starGift.canUpgrade && !message.isOutgoing ? lang('ActionStarGiftUnpack') : oldLang('ActionGiftPremiumView')} +
{starGift.gift.availabilityTotal && ( { closeGiftInfoModal(); }); + const handleFocusUpgraded = useLastCallback(() => { + if (!userGift?.upgradeMsgId) return; + const { upgradeMsgId, fromId } = userGift; + focusMessage({ chatId: fromId!, messageId: upgradeMsgId! }); + handleClose(); + }); + const handleTriggerVisibility = useLastCallback(() => { const { fromId, messageId, isUnsaved } = userGift!; changeGiftVisilibity({ userId: fromId!, messageId: messageId!, shouldUnsave: !isUnsaved }); @@ -201,7 +212,7 @@ const GiftInfoModal = ({ @@ -429,7 +440,12 @@ const GiftInfoModal = ({ )} )} - {!canUpdate && ( + {canFocusUpgrade && ( + + )} + {!canUpdate && !canFocusUpgrade && ( @@ -449,7 +465,7 @@ const GiftInfoModal = ({ }; }, [ typeGift, userGift, targetUser, giftSticker, lang, canUpdate, canConvertDifference, isSender, oldLang, gift, - radialPatternBackdrop, giftAttributes, + radialPatternBackdrop, giftAttributes, canFocusUpgrade, ]); return ( diff --git a/src/global/actions/ui/stars.ts b/src/global/actions/ui/stars.ts index 3777b8b12..19ea57c4d 100644 --- a/src/global/actions/ui/stars.ts +++ b/src/global/actions/ui/stars.ts @@ -263,6 +263,7 @@ addActionHandler('openGiftInfoModalFromMessage', (global, actions, payload): Act fromId: message.isOutgoing ? global.currentUserId : message.chatId, messageId: (!message.isOutgoing || chatId === global.currentUserId) ? message.id : undefined, isConverted: starGift.isConverted, + upgradeMsgId: starGift.upgradeMsgId, } satisfies ApiUserStarGift; actions.openGiftInfoModal({ userId: giftReceiverId, gift, tabId }); diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 97d0733c0..d711708d1 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1138,6 +1138,7 @@ export interface LangPair { 'GiftAttributeSymbol': undefined; 'GiftInfoStatus': undefined; 'GiftInfoStatusNonUnique': undefined; + 'GiftInfoViewUpgraded': undefined; 'AllGiftsCategory': undefined; 'LimitedGiftsCategory': undefined; 'StockGiftsCategory': undefined; @@ -1145,6 +1146,9 @@ export interface LangPair { 'StarsReactionLinkText': undefined; 'StarsReactionLink': undefined; 'ActionStarGiftDisplaying': undefined; + 'ActionStarGiftDescriptionUpgrade': undefined; + 'ActionStarGiftUpgraded': undefined; + 'ActionStarGiftUnpack': undefined; 'GiftTo': undefined; 'GiftFrom': undefined; 'ReceivedGift': undefined; @@ -1212,6 +1216,8 @@ export interface LangPair { 'ProfileTabVoice': undefined; 'ProfileTabSharedGroups': undefined; 'ProfileTabSimilarChannels': undefined; + 'ActionUnsupportedTitle': undefined; + 'ActionUnsupportedDescription': undefined; } export interface LangPairWithVariables { @@ -1612,6 +1618,9 @@ export interface LangPairWithVariables { 'ActionStarGiftDescription': { 'count': V; }; + 'ActionStarGiftOutDescriptionUpgrade': { + 'user': V; + }; 'StarGiftInfoDescriptionInbound': { 'count': V; 'link': V; @@ -1634,9 +1643,6 @@ export interface LangPairWithVariables { 'EmojiStatusAccessText': { 'name': V; }; - 'LocationPermissionText': { - 'name': V; - }; 'BotSuggestedStatusFor': { 'bot': V; 'duration': V; @@ -1671,6 +1677,9 @@ export interface LangPairWithVariables { 'FolderLinkNotificationUpdatedTitle': { 'title': V; }; + 'LocationPermissionText': { + 'name': V; + }; } export interface LangPairPlural {