From 1f834d42edbbc6fe16128ffa3551b3d03b2f3bde Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 3 May 2024 14:38:23 +0200 Subject: [PATCH] Profile: Show collectible info (#4505) --- src/api/gramjs/apiBuilders/messages.ts | 8 +- src/api/gramjs/apiBuilders/misc.ts | 21 +++ src/api/gramjs/methods/fragment.ts | 26 +++ src/api/gramjs/methods/index.ts | 2 + src/api/types/messages.ts | 2 +- src/api/types/misc.ts | 9 + src/assets/tgs/general/Fragment.tgs | Bin 0 -> 3472 bytes src/assets/tgs/general/Mention.tgs | Bin 0 -> 4492 bytes src/bundles/extra.ts | 1 + .../common/helpers/animatedAssets.ts | 4 + .../common/helpers/formatUsername.ts | 5 + .../helpers/renderActionMessageText.tsx | 3 +- src/components/common/profile/ChatExtra.tsx | 53 ++++-- src/components/modals/ModalContainer.tsx | 3 + .../CollectibleInfoModal.async.tsx | 18 ++ .../CollectibleInfoModal.module.scss | 33 ++++ .../collectible/CollectibleInfoModal.tsx | 156 ++++++++++++++++++ src/config.ts | 2 + src/global/actions/api/chats.ts | 33 ++++ src/global/actions/ui/misc.ts | 7 + src/global/types.ts | 14 ++ src/lib/gramjs/tl/apiTl.js | 3 +- src/lib/gramjs/tl/static/api.json | 3 +- src/util/formatCurrency.ts | 3 + 24 files changed, 382 insertions(+), 27 deletions(-) create mode 100644 src/api/gramjs/methods/fragment.ts create mode 100644 src/assets/tgs/general/Fragment.tgs create mode 100644 src/assets/tgs/general/Mention.tgs create mode 100644 src/components/common/helpers/formatUsername.ts create mode 100644 src/components/modals/collectible/CollectibleInfoModal.async.tsx create mode 100644 src/components/modals/collectible/CollectibleInfoModal.module.scss create mode 100644 src/components/modals/collectible/CollectibleInfoModal.tsx diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 4f9752eff..dfdb68dd8 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -336,7 +336,7 @@ function buildAction( let currency: string | undefined; let giftCryptoInfo: { currency: string; - amount: string; + amount: number; } | undefined; let text: string; const translationValues: string[] = []; @@ -499,10 +499,9 @@ function buildAction( } currency = action.currency; if (action.cryptoCurrency) { - const cryptoAmountWithDecimals = action.cryptoAmount!.divide(1e7).toJSNumber() / 100; giftCryptoInfo = { currency: action.cryptoCurrency, - amount: cryptoAmountWithDecimals.toFixed(2), + amount: action.cryptoAmount!.toJSNumber(), }; } amount = action.amount.toJSNumber(); @@ -552,10 +551,9 @@ function buildAction( } currency = action.currency; if (action.cryptoCurrency) { - const cryptoAmountWithDecimals = action.cryptoAmount!.divide(1e7).toJSNumber() / 100; giftCryptoInfo = { currency: action.cryptoCurrency, - amount: cryptoAmountWithDecimals.toFixed(2), + amount: action.cryptoAmount!.toJSNumber(), }; } if (action.boostPeer) { diff --git a/src/api/gramjs/apiBuilders/misc.ts b/src/api/gramjs/apiBuilders/misc.ts index 1ab04a708..dc0755ac5 100644 --- a/src/api/gramjs/apiBuilders/misc.ts +++ b/src/api/gramjs/apiBuilders/misc.ts @@ -3,6 +3,7 @@ import { Api as GramJs } from '../../../lib/gramjs'; import type { ApiPrivacyKey } from '../../../types'; import type { ApiChatLink, + ApiCollectionInfo, ApiConfig, ApiCountry, ApiLangString, ApiPeerColors, ApiSession, ApiTimezone, ApiUrlAuthResult, ApiWallpaper, ApiWebSession, @@ -274,3 +275,23 @@ export function buildApiChatLink(data: GramJs.account.ResolvedBusinessChatLinks) text: buildMessageTextContent(data.message, data.entities), }; } + +export function buildApiCollectibleInfo(info: GramJs.fragment.TypeCollectibleInfo): ApiCollectionInfo { + const { + amount, + currency, + cryptoAmount, + cryptoCurrency, + purchaseDate, + url, + } = info; + + return { + amount: amount.toJSNumber(), + currency, + cryptoAmount: cryptoAmount.toJSNumber(), + cryptoCurrency, + purchaseDate, + url, + }; +} diff --git a/src/api/gramjs/methods/fragment.ts b/src/api/gramjs/methods/fragment.ts new file mode 100644 index 000000000..4709cb053 --- /dev/null +++ b/src/api/gramjs/methods/fragment.ts @@ -0,0 +1,26 @@ +import { Api as GramJs } from '../../../lib/gramjs'; + +import { buildApiCollectibleInfo } from '../apiBuilders/misc'; +import { invokeRequest } from './client'; + +type InputCollectible = { + phone: string; +} | { + username: string; +}; + +export async function fetchCollectionInfo(collectible: InputCollectible) { + const inputCollectible = 'username' in collectible + ? new GramJs.InputCollectibleUsername({ username: collectible.username }) + : new GramJs.InputCollectiblePhone({ phone: collectible.phone }); + + const result = await invokeRequest(new GramJs.fragment.GetCollectibleInfo({ + collectible: inputCollectible, + })); + + if (!result) { + return undefined; + } + + return buildApiCollectibleInfo(result); +} diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index b448d0ee7..42bec0586 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -102,3 +102,5 @@ export { applyBoost, fetchBoostList, fetchBoostStatus, fetchGiveawayInfo, fetchMyBoosts, applyGiftCode, checkGiftCode, getPremiumGiftCodeOptions, launchPrepaidGiveaway, } from './payments'; + +export * from './fragment'; diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 7f3e794b2..9b9da973a 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -351,7 +351,7 @@ export interface ApiAction { currency?: string; giftCryptoInfo?: { currency: string; - amount: string; + amount: number; }; translationValues: string[]; call?: Partial; diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 6848ef05e..bf664b562 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -267,3 +267,12 @@ type ApiUrlAuthResultDefault = { }; export type ApiUrlAuthResult = ApiUrlAuthResultRequest | ApiUrlAuthResultAccepted | ApiUrlAuthResultDefault; + +export interface ApiCollectionInfo { + amount: number; + currency: string; + cryptoAmount: number; + cryptoCurrency: string; + purchaseDate: number; + url: string; +} diff --git a/src/assets/tgs/general/Fragment.tgs b/src/assets/tgs/general/Fragment.tgs new file mode 100644 index 0000000000000000000000000000000000000000..bcd1af2d0f458c4713bc9f207f6c786f53664c0c GIT binary patch literal 3472 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 literal 0 HcmV?d00001 diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index c315f4df1..a70f74f04 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -39,6 +39,7 @@ export { default as CountryPickerModal } from '../components/common/CountryPicke export { default as ReactorListModal } from '../components/middle/ReactorListModal'; export { default as EmojiInteractionAnimation } from '../components/middle/EmojiInteractionAnimation'; export { default as ChatLanguageModal } from '../components/middle/ChatLanguageModal'; +export { default as CollectibleInfoModal } from '../components/modals/collectible/CollectibleInfoModal'; export { default as LeftSearch } from '../components/left/search/LeftSearch'; export { default as Settings } from '../components/left/settings/Settings'; diff --git a/src/components/common/helpers/animatedAssets.ts b/src/components/common/helpers/animatedAssets.ts index b87170fc0..0608051fd 100644 --- a/src/components/common/helpers/animatedAssets.ts +++ b/src/components/common/helpers/animatedAssets.ts @@ -8,6 +8,8 @@ import VoiceMini from '../../../assets/tgs/calls/VoiceMini.tgs'; import VoiceMuted from '../../../assets/tgs/calls/VoiceMuted.tgs'; import VoiceOutlined from '../../../assets/tgs/calls/VoiceOutlined.tgs'; import Flame from '../../../assets/tgs/general/Flame.tgs'; +import Fragment from '../../../assets/tgs/general/Fragment.tgs'; +import Mention from '../../../assets/tgs/general/Mention.tgs'; import PartyPopper from '../../../assets/tgs/general/PartyPopper.tgs'; import Invite from '../../../assets/tgs/invites/Invite.tgs'; import JoinRequest from '../../../assets/tgs/invites/Requests.tgs'; @@ -54,4 +56,6 @@ export const LOCAL_TGS_URLS = { ReadTime, Unlock, LastSeen, + Mention, + Fragment, }; diff --git a/src/components/common/helpers/formatUsername.ts b/src/components/common/helpers/formatUsername.ts new file mode 100644 index 000000000..4c114a7b1 --- /dev/null +++ b/src/components/common/helpers/formatUsername.ts @@ -0,0 +1,5 @@ +import { TME_LINK_PREFIX } from '../../../config'; + +export default function formatUsername(username: string, asAbsoluteLink?: boolean) { + return asAbsoluteLink ? `${TME_LINK_PREFIX}${username}` : `@${username}`; +} diff --git a/src/components/common/helpers/renderActionMessageText.tsx b/src/components/common/helpers/renderActionMessageText.tsx index 0ea61633c..1cbce3c92 100644 --- a/src/components/common/helpers/renderActionMessageText.tsx +++ b/src/components/common/helpers/renderActionMessageText.tsx @@ -138,7 +138,8 @@ export function renderActionMessageText( let priceText = price; if (giftCryptoInfo) { - priceText = `${giftCryptoInfo.amount} ${giftCryptoInfo.currency} (~${price})`; + const cryptoPrice = formatCurrency(giftCryptoInfo.amount, giftCryptoInfo.currency, lang.code); + priceText = `${cryptoPrice} (${price})`; } processed = processPlaceholder( diff --git a/src/components/common/profile/ChatExtra.tsx b/src/components/common/profile/ChatExtra.tsx index d12f5eb50..f20c35ebb 100644 --- a/src/components/common/profile/ChatExtra.tsx +++ b/src/components/common/profile/ChatExtra.tsx @@ -9,7 +9,7 @@ import type { } from '../../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types'; -import { TME_LINK_PREFIX } from '../../../config'; +import { FRAGMENT_PHONE_CODE, FRAGMENT_PHONE_LENGTH } from '../../../config'; import { buildStaticMapHash, getChatLink, @@ -33,6 +33,7 @@ import { formatPhoneNumberWithCode } from '../../../util/phoneNumber'; import { debounce } from '../../../util/schedulers'; import stopEvent from '../../../util/stopEvent'; import { ChatAnimationTypes } from '../../left/main/hooks'; +import formatUsername from '../helpers/formatUsername'; import renderText from '../helpers/renderText'; import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps'; @@ -102,6 +103,7 @@ const ChatExtra: FC = ({ loadPeerStories, openSavedDialog, openMapModal, + requestCollectibleInfo, } = getActions(); const { @@ -206,10 +208,6 @@ const ChatExtra: FC = ({ openSavedDialog({ chatId: chatOrUserId }); }); - if (!chat || chat.isRestricted || (isSelf && !isInSettings)) { - return undefined; - } - function copy(text: string, entity: string) { copyTextToClipboard(text); showNotification({ message: `${entity} was copied` }); @@ -217,6 +215,26 @@ const ChatExtra: FC = ({ const formattedNumber = phoneNumber && formatPhoneNumberWithCode(phoneCodeList, phoneNumber); + const handlePhoneClick = useLastCallback(() => { + if (phoneNumber?.length === FRAGMENT_PHONE_LENGTH && phoneNumber.startsWith(FRAGMENT_PHONE_CODE)) { + requestCollectibleInfo({ collectible: phoneNumber, userId: userId!, type: 'phone' }); + return; + } + copy(formattedNumber!, lang('Phone')); + }); + + const handleUsernameClick = useLastCallback((username: ApiUsername, isChat?: boolean) => { + if (!username.isEditable) { + requestCollectibleInfo({ collectible: username.username, userId: userId!, type: 'username' }); + return; + } + copy(formatUsername(username.username, isChat), lang(isChat ? 'Link' : 'Username')); + }); + + if (!chat || chat.isRestricted || (isSelf && !isInSettings)) { + return undefined; + } + function renderUsernames(usernameList: ApiUsername[], isChat?: boolean) { const [mainUsername, ...otherUsernames] = usernameList; @@ -226,21 +244,21 @@ const ChatExtra: FC = ({ .map((s) => { return (s === 'USERNAMES' ? ( <> - {otherUsernames.map(({ username: nick }, idx) => { - const textToCopy = isChat ? `${TME_LINK_PREFIX}${nick}` : `@${nick}`; + {otherUsernames.map((username, idx) => { return ( <> {idx > 0 ? ', ' : ''} { stopEvent(e); - copy(textToCopy, lang(isChat ? 'Link' : 'Username')); + handleUsernameClick(username, isChat); }} className="text-entity-link username-link" > - {`@${nick}`} + {formatUsername(username.username)} ); @@ -250,9 +268,6 @@ const ChatExtra: FC = ({ }) : undefined; - const username = isChat ? `t.me/${mainUsername.username}` : mainUsername.username; - const textToCopy = isChat ? `${TME_LINK_PREFIX}${mainUsername.username}` : `@${mainUsername.username}`; - return ( = ({ narrow ripple // eslint-disable-next-line react/jsx-no-bind - onClick={() => copy(textToCopy, lang(isChat ? 'Link' : 'Username'))} + onClick={() => { + handleUsernameClick(mainUsername, isChat); + }} > - {username} + {formatUsername(mainUsername.username, isChat)} {usernameLinks && {usernameLinks}} {lang(isChat ? 'Link' : 'Username')} @@ -289,9 +306,9 @@ const ChatExtra: FC = ({ /> )} - {formattedNumber && Boolean(formattedNumber.length) && ( + {Boolean(formattedNumber?.length) && ( // eslint-disable-next-line react/jsx-no-bind - copy(formattedNumber, lang('Phone'))}> + {formattedNumber} {lang('Phone')} diff --git a/src/components/modals/ModalContainer.tsx b/src/components/modals/ModalContainer.tsx index 3031de69e..08335c614 100644 --- a/src/components/modals/ModalContainer.tsx +++ b/src/components/modals/ModalContainer.tsx @@ -9,6 +9,7 @@ import { pick } from '../../util/iteratees'; import AttachBotInstallModal from './attachBotInstall/AttachBotInstallModal.async'; import BoostModal from './boost/BoostModal.async'; import ChatlistModal from './chatlist/ChatlistModal.async'; +import CollectibleInfoModal from './collectible/CollectibleInfoModal.async'; import GiftCodeModal from './giftcode/GiftCodeModal.async'; import InviteViaLinkModal from './inviteViaLink/InviteViaLinkModal.async'; import OneTimeMediaModal from './oneTimeMedia/OneTimeMediaModal.async'; @@ -25,6 +26,7 @@ type ModalKey = keyof Pick; @@ -51,6 +53,7 @@ const MODALS: ModalRegistry = { requestedAttachBotInstall: AttachBotInstallModal, reportAdModal: ReportAdModal, webApp: WebAppModal, + collectibleInfoModal: CollectibleInfoModal, }; const MODAL_KEYS = Object.keys(MODALS) as ModalKey[]; const MODAL_ENTRIES = Object.entries(MODALS) as Entries; diff --git a/src/components/modals/collectible/CollectibleInfoModal.async.tsx b/src/components/modals/collectible/CollectibleInfoModal.async.tsx new file mode 100644 index 000000000..0dd162896 --- /dev/null +++ b/src/components/modals/collectible/CollectibleInfoModal.async.tsx @@ -0,0 +1,18 @@ +import type { FC } from '../../../lib/teact/teact'; +import React from '../../../lib/teact/teact'; + +import type { OwnProps } from './CollectibleInfoModal'; + +import { Bundles } from '../../../util/moduleLoader'; + +import useModuleLoader from '../../../hooks/useModuleLoader'; + +const CollectibleInfoModalAsync: FC = (props) => { + const { modal } = props; + const CollectibleInfoModal = useModuleLoader(Bundles.Extra, 'CollectibleInfoModal', !modal); + + // eslint-disable-next-line react/jsx-props-no-spreading + return CollectibleInfoModal ? : undefined; +}; + +export default CollectibleInfoModalAsync; diff --git a/src/components/modals/collectible/CollectibleInfoModal.module.scss b/src/components/modals/collectible/CollectibleInfoModal.module.scss new file mode 100644 index 000000000..f9a50f4a1 --- /dev/null +++ b/src/components/modals/collectible/CollectibleInfoModal.module.scss @@ -0,0 +1,33 @@ +.content { + display: flex; + flex-direction: column; + align-items: center; +} + +.closeButton { + position: absolute; + top: 0.5rem; + left: 0.5rem; +} + +.icon { + width: 5rem; + height: 5rem; + border-radius: 50%; + + display: grid; + place-items: center; + + flex-shrink: 0; + background-color: var(--color-primary); +} + +.title, .description { + text-align: center !important; + text-wrap: pretty; + padding: 0 1rem; +} + +.title { + margin-top: 1rem; +} diff --git a/src/components/modals/collectible/CollectibleInfoModal.tsx b/src/components/modals/collectible/CollectibleInfoModal.tsx new file mode 100644 index 000000000..7724b59e1 --- /dev/null +++ b/src/components/modals/collectible/CollectibleInfoModal.tsx @@ -0,0 +1,156 @@ +import type { FC } from '../../../lib/teact/teact'; +import React, { + memo, + useMemo, +} from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; + +import type { ApiCountryCode } from '../../../api/types'; +import type { TabState } from '../../../global/types'; + +import { copyTextToClipboard } from '../../../util/clipboard'; +import { formatDateAtTime } from '../../../util/date/dateFormat'; +import { formatCurrency } from '../../../util/formatCurrency'; +import { formatPhoneNumberWithCode } from '../../../util/phoneNumber'; +import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets'; +import formatUsername from '../../common/helpers/formatUsername'; +import renderText from '../../common/helpers/renderText'; + +import useLang from '../../../hooks/useLang'; +import useLastCallback from '../../../hooks/useLastCallback'; + +import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview'; +import Icon from '../../common/Icon'; +import PickerSelectedItem from '../../common/PickerSelectedItem'; +import Button from '../../ui/Button'; +import Modal from '../../ui/Modal'; + +import styles from './CollectibleInfoModal.module.scss'; + +export type OwnProps = { + modal: TabState['collectibleInfoModal']; +}; + +type StateProps = { + phoneCodeList: ApiCountryCode[]; +}; + +const TOP_ICON_SIZE = 60; + +const CollectibleInfoModal: FC = ({ + modal, + phoneCodeList, +}) => { + const { + closeCollectibleInfoModal, + openChat, + openUrl, + showNotification, + } = getActions(); + const lang = useLang(); + + const isUsername = modal?.type === 'username'; + + const handleClose = useLastCallback(() => { + closeCollectibleInfoModal(); + }); + + const handleOpenChat = useLastCallback(() => { + openChat({ id: modal!.userId }); + handleClose(); + }); + + const handleOpenUrl = useLastCallback(() => { + openUrl({ + url: modal!.url, + shouldSkipModal: true, + }); + handleClose(); + }); + + const handleCopy = useLastCallback(() => { + const text = isUsername ? formatUsername(modal!.collectible) + : formatPhoneNumberWithCode(phoneCodeList, modal!.collectible); + copyTextToClipboard(text); + showNotification({ + message: lang(isUsername ? 'UsernameCopied' : 'PhoneCopied'), + }); + handleClose(); + }); + + const title = useMemo(() => { + if (!modal) return undefined; + const key = isUsername ? 'FragmentUsernameTitle' : 'FragmentPhoneTitle'; + const formattedCollectible = isUsername + ? formatUsername(modal.collectible) + : formatPhoneNumberWithCode(phoneCodeList, modal.collectible); + return lang(key, formattedCollectible); + }, [modal, isUsername, phoneCodeList, lang]); + + const description = useMemo(() => { + if (!modal) return undefined; + const key = isUsername ? 'FragmentUsernameMessage' : 'FragmentPhoneMessage'; + const date = formatDateAtTime(lang, modal.purchaseDate * 1000); + const currency = formatCurrency(modal.amount, modal.currency, lang.code); + const cryptoCurrency = formatCurrency(modal.cryptoAmount, modal.cryptoCurrency, lang.code); + const paid = `${cryptoCurrency} (${currency})`; + return lang(key, [date, paid]); + }, [modal, isUsername, lang]); + + return ( + + +
+ +
+

+ {title && renderText(title, ['simple_markdown'])} +

+ +

+ {description && renderText(description, ['simple_markdown'])} +

+
+ + +
+
+ ); +}; + +export default memo(withGlobal( + (global): StateProps => { + const { countryList } = global; + + return { + phoneCodeList: countryList.phoneCodes, + }; + }, +)(CollectibleInfoModal)); diff --git a/src/config.ts b/src/config.ts index 923eb364d..bf0653b3a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -319,6 +319,8 @@ export const GIVEAWAY_MAX_ADDITIONAL_CHANNELS = 10; export const GIVEAWAY_MAX_ADDITIONAL_USERS = 10; export const GIVEAWAY_MAX_ADDITIONAL_COUNTRIES = 10; export const BOOST_PER_SENT_GIFT = 3; +export const FRAGMENT_PHONE_CODE = '888'; +export const FRAGMENT_PHONE_LENGTH = 11; export const LIGHT_THEME_BG_COLOR = '#99BA92'; export const DARK_THEME_BG_COLOR = '#0F0F0F'; diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 26c6679ce..d54ba2c9a 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -30,6 +30,7 @@ import { TOPICS_SLICE, TOPICS_SLICE_SECOND_LOAD, } from '../../../config'; +import { copyTextToClipboard } from '../../../util/clipboard'; import { formatShareText, parseChooseParameter, processDeepLink } from '../../../util/deeplink'; import { isDeepLink } from '../../../util/deepLinkParser'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; @@ -2673,6 +2674,38 @@ addActionHandler('resolveBusinessChatLink', async (global, actions, payload): Pr }); }); +addActionHandler('requestCollectibleInfo', async (global, actions, payload): Promise => { + const { + type, collectible, userId, tabId = getCurrentTabId(), + } = payload; + + let inputCollectible; + if (type === 'phone') { + inputCollectible = { phone: collectible }; + } + if (type === 'username') { + inputCollectible = { username: collectible }; + } + if (!inputCollectible) return; + + const result = await callApi('fetchCollectionInfo', inputCollectible); + if (!result) { + copyTextToClipboard(collectible); + return; + } + + global = getGlobal(); + global = updateTabState(global, { + collectibleInfoModal: { + ...result, + type, + collectible, + userId, + }, + }, tabId); + setGlobal(global); +}); + async function loadChats( listType: ChatListType, offsetId?: string, diff --git a/src/global/actions/ui/misc.ts b/src/global/actions/ui/misc.ts index 7cd44a83f..20286d368 100644 --- a/src/global/actions/ui/misc.ts +++ b/src/global/actions/ui/misc.ts @@ -744,6 +744,13 @@ addActionHandler('closeInviteViaLinkModal', (global, actions, payload): ActionRe }, tabId); }); +addActionHandler('closeCollectibleInfoModal', (global, actions, payload): ActionReturnType => { + const { tabId = getCurrentTabId() } = payload ?? {}; + return updateTabState(global, { + collectibleInfoModal: undefined, + }, tabId); +}); + addActionHandler('setShouldCloseRightColumn', (global, actions, payload): ActionReturnType => { const { value, tabId = getCurrentTabId() } = payload; return updateTabState(global, { diff --git a/src/global/types.ts b/src/global/types.ts index c82a2630f..a46e9b0e0 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -16,6 +16,7 @@ import type { ApiChatReactions, ApiChatType, ApiCheckedGiftCode, + ApiCollectionInfo, ApiConfig, ApiContact, ApiCountry, @@ -738,6 +739,12 @@ export type TabState = { oneTimeMediaModal?: { message: ApiMessage; }; + + collectibleInfoModal?: ApiCollectionInfo & { + userId: string; + type: 'phone' | 'username'; + collectible: string; + }; }; export type GlobalState = { @@ -2867,6 +2874,13 @@ export interface ActionPayloads { openOneTimeMediaModal: TabState['oneTimeMediaModal'] & WithTabId; closeOneTimeMediaModal: WithTabId | undefined; + requestCollectibleInfo: { + userId: string; + type : 'phone' | 'username'; + collectible: string; + } & WithTabId; + closeCollectibleInfoModal: WithTabId | undefined; + // Calls joinGroupCall: { chatId?: string; diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index b662ac747..6d9530d49 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -1637,4 +1637,5 @@ stories.togglePeerStoriesHidden#bd0415c4 peer:InputPeer hidden:Bool = Bool; premium.getBoostsList#60f67660 flags:# gifts:flags.0?true peer:InputPeer offset:string limit:int = premium.BoostsList; premium.getMyBoosts#be77b4a = premium.MyBoosts; premium.applyBoost#6b7da746 flags:# slots:flags.0?Vector peer:InputPeer = premium.MyBoosts; -premium.getBoostsStatus#42f1f61 peer:InputPeer = premium.BoostsStatus;`; \ No newline at end of file +premium.getBoostsStatus#42f1f61 peer:InputPeer = premium.BoostsStatus; +fragment.getCollectibleInfo#be1e85ba collectible:InputCollectible = fragment.CollectibleInfo;`; \ No newline at end of file diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index c50389935..676642104 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -349,5 +349,6 @@ "payments.applyGiftCode", "payments.getGiveawayInfo", "payments.getPremiumGiftCodeOptions", - "payments.launchPrepaidGiveaway" + "payments.launchPrepaidGiveaway", + "fragment.getCollectibleInfo" ] diff --git a/src/util/formatCurrency.ts b/src/util/formatCurrency.ts index 7bc07c478..4a7bf9468 100644 --- a/src/util/formatCurrency.ts +++ b/src/util/formatCurrency.ts @@ -24,6 +24,9 @@ export function formatCurrency( } function getCurrencyExp(currency: string) { + if (currency === 'TON') { + return 9; + } if (currency === 'CLF') { return 4; }