diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 7909de94c..196ee6b2d 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -698,7 +698,6 @@ function buildReplyButtons(message: UniversalMessage, shouldSkipBuyButton?: bool if (media instanceof GramJs.MessageMediaInvoice && media.receiptMsgId) { return { type: 'receipt', - text: 'PaymentReceipt', receiptMessageId: media.receiptMsgId, }; } diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 75bc279c7..e338fc806 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -726,7 +726,6 @@ interface ApiKeyboardButtonSimple { interface ApiKeyboardButtonReceipt { type: 'receipt'; - text: string; receiptMessageId: number; } diff --git a/src/components/common/helpers/renderActionMessageText.tsx b/src/components/common/helpers/renderActionMessageText.tsx index bb0418c16..f6c25cc3c 100644 --- a/src/components/common/helpers/renderActionMessageText.tsx +++ b/src/components/common/helpers/renderActionMessageText.tsx @@ -14,7 +14,7 @@ import { isExpiredMessage, } from '../../../global/helpers'; import { getMessageSummaryText } from '../../../global/helpers/messageSummary'; -import { formatCurrency } from '../../../util/formatCurrency'; +import { formatCurrencyAsString } from '../../../util/formatCurrency'; import trimText from '../../../util/trimText'; import renderText from './renderText'; @@ -85,7 +85,7 @@ export function renderActionMessageText( processed = processPlaceholder( unprocessed, '%payment_amount%', - formatCurrency(amount!, currency!, lang.code), + formatCurrencyAsString(amount!, currency!, lang.code), ); unprocessed = processed.pop() as string; content.push(...processed); @@ -134,11 +134,11 @@ export function renderActionMessageText( } if (unprocessed.includes('%gift_payment_amount%')) { - const price = formatCurrency(amount!, currency!, lang.code); + const price = formatCurrencyAsString(amount!, currency!, lang.code); let priceText = price; if (giftCryptoInfo) { - const cryptoPrice = formatCurrency(giftCryptoInfo.amount, giftCryptoInfo.currency, lang.code); + const cryptoPrice = formatCurrencyAsString(giftCryptoInfo.amount, giftCryptoInfo.currency, lang.code); priceText = `${cryptoPrice} (${price})`; } diff --git a/src/components/common/icons/StarIcon.module.scss b/src/components/common/icons/StarIcon.module.scss index 29e2efd26..b889fe271 100644 --- a/src/components/common/icons/StarIcon.module.scss +++ b/src/components/common/icons/StarIcon.module.scss @@ -17,6 +17,14 @@ height: 1.5rem; } +.adaptive { + display: inline-block; + width: 1em; + height: 1em; + line-height: 1; + vertical-align: text-top; +} + .svg { width: 100%; height: 100%; diff --git a/src/components/common/icons/StarIcon.tsx b/src/components/common/icons/StarIcon.tsx index 2a24e6518..8721cc9d4 100644 --- a/src/components/common/icons/StarIcon.tsx +++ b/src/components/common/icons/StarIcon.tsx @@ -9,7 +9,7 @@ import styles from './StarIcon.module.scss'; type OwnProps = { type?: 'gold' | 'premium' | 'regular'; - size?: 'small' | 'middle' | 'big'; + size?: 'small' | 'middle' | 'big' | 'adaptive'; className?: string; onClick?: VoidFunction; }; diff --git a/src/components/middle/HeaderPinnedMessage.tsx b/src/components/middle/HeaderPinnedMessage.tsx index 67be88b86..95d2ce402 100644 --- a/src/components/middle/HeaderPinnedMessage.tsx +++ b/src/components/middle/HeaderPinnedMessage.tsx @@ -12,6 +12,7 @@ import buildClassName from '../../util/buildClassName'; import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; import { getPictogramDimensions, REM } from '../common/helpers/mediaDimensions'; import renderText from '../common/helpers/renderText'; +import renderKeyboardButtonText from './composer/helpers/renderKeyboardButtonText'; import { useFastClick } from '../../hooks/useFastClick'; import useFlag from '../../hooks/useFlag'; @@ -198,7 +199,7 @@ const HeaderPinnedMessage: FC = ({ onMouseEnter={!IS_TOUCH_ENV ? markNoHoverColor : undefined} onMouseLeave={!IS_TOUCH_ENV ? unmarkNoHoverColor : undefined} > - {renderText(inlineButton.text)} + {renderKeyboardButtonText(lang, inlineButton)} )} diff --git a/src/components/middle/composer/BotKeyboardMenu.tsx b/src/components/middle/composer/BotKeyboardMenu.tsx index 3ec717eec..9198f9799 100644 --- a/src/components/middle/composer/BotKeyboardMenu.tsx +++ b/src/components/middle/composer/BotKeyboardMenu.tsx @@ -1,13 +1,15 @@ -import type { FC } from '../../../lib/teact/teact'; -import React, { memo } from '../../../lib/teact/teact'; +import type { FC, TeactNode } from '../../../lib/teact/teact'; +import React, { memo, useMemo } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; import type { ApiMessage } from '../../../api/types'; import { selectChatMessage, selectCurrentMessageList } from '../../../global/selectors'; import { IS_TOUCH_ENV } from '../../../util/windowEnvironment'; +import renderKeyboardButtonText from './helpers/renderKeyboardButtonText'; import useMouseInside from '../../../hooks/useMouseInside'; +import useOldLang from '../../../hooks/useOldLang'; import Button from '../../ui/Button'; import Menu from '../../ui/Menu'; @@ -29,9 +31,20 @@ const BotKeyboardMenu: FC = ({ }) => { const { clickBotInlineButton } = getActions(); + const lang = useOldLang(); + const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose); const { isKeyboardSingleUse } = message || {}; + const buttonTexts = useMemo(() => { + const texts: TeactNode[][] = []; + message?.keyboardButtons!.forEach((row) => { + texts.push(row.map((button) => renderKeyboardButtonText(lang, button))); + }); + + return texts; + }, [lang, message?.keyboardButtons]); + if (!message || !message.keyboardButtons) { return undefined; } @@ -50,16 +63,16 @@ const BotKeyboardMenu: FC = ({ noCompact >
- {message.keyboardButtons.map((row) => ( + {message.keyboardButtons.map((row, i) => (
- {row.map((button) => ( + {row.map((button, j) => ( ))}
diff --git a/src/components/middle/composer/helpers/renderKeyboardButtonText.tsx b/src/components/middle/composer/helpers/renderKeyboardButtonText.tsx new file mode 100644 index 000000000..2ef1fc9e3 --- /dev/null +++ b/src/components/middle/composer/helpers/renderKeyboardButtonText.tsx @@ -0,0 +1,22 @@ +import React, { type TeactNode } from '../../../../lib/teact/teact'; + +import type { ApiKeyboardButton } from '../../../../api/types'; + +import { replaceWithTeact } from '../../../../util/replaceWithTeact'; +import renderText from '../../../common/helpers/renderText'; + +import { type LangFn } from '../../../../hooks/useOldLang'; + +import Icon from '../../../common/icons/Icon'; + +export default function renderKeyboardButtonText(lang: LangFn, button: ApiKeyboardButton): TeactNode { + if (button.type === 'receipt') { + return lang('PaymentReceipt'); + } + + if (button.type === 'buy') { + return replaceWithTeact(button.text, '⭐', ); + } + + return renderText(button.text); +} diff --git a/src/components/middle/message/InlineButtons.scss b/src/components/middle/message/InlineButtons.scss index 710897967..ead0e0094 100644 --- a/src/components/middle/message/InlineButtons.scss +++ b/src/components/middle/message/InlineButtons.scss @@ -37,7 +37,7 @@ margin-right: 0; } - .icon { + .corner-icon { font-size: 0.875rem; position: absolute; right: 0.1875rem; diff --git a/src/components/middle/message/InlineButtons.tsx b/src/components/middle/message/InlineButtons.tsx index c43d43780..cf5be9c5b 100644 --- a/src/components/middle/message/InlineButtons.tsx +++ b/src/components/middle/message/InlineButtons.tsx @@ -1,13 +1,14 @@ -import type { FC } from '../../../lib/teact/teact'; -import React from '../../../lib/teact/teact'; +import type { FC, TeactNode } from '../../../lib/teact/teact'; +import React, { memo, useMemo } from '../../../lib/teact/teact'; import type { ApiKeyboardButton, ApiMessage } from '../../../api/types'; import { RE_TME_LINK } from '../../../config'; -import renderText from '../../common/helpers/renderText'; +import renderKeyboardButtonText from '../composer/helpers/renderKeyboardButtonText'; import useOldLang from '../../../hooks/useOldLang'; +import Icon from '../../common/icons/Icon'; import Button from '../../ui/Button'; import './InlineButtons.scss'; @@ -25,29 +26,37 @@ const InlineButtons: FC = ({ message, onClick }) => { switch (type) { case 'url': { if (!RE_TME_LINK.test(button.url)) { - return ; + return ; } break; } case 'urlAuth': - return ; + return ; case 'buy': case 'receipt': - return ; + return ; case 'switchBotInline': - return ; + return ; case 'webView': case 'simpleWebView': - return ; + return ; } return undefined; }; + const buttonTexts = useMemo(() => { + const texts: TeactNode[][] = []; + message.inlineButtons!.forEach((row) => { + texts.push(row.map((button) => renderKeyboardButtonText(lang, button))); + }); + return texts; + }, [lang, message.inlineButtons]); + return (
- {message.inlineButtons!.map((row) => ( + {message.inlineButtons!.map((row, i) => (
- {row.map((button) => ( + {row.map((button, j) => ( ))} @@ -65,4 +76,4 @@ const InlineButtons: FC = ({ message, onClick }) => { ); }; -export default InlineButtons; +export default memo(InlineButtons); diff --git a/src/components/middle/message/Invoice.scss b/src/components/middle/message/Invoice.scss index 111ca5a5c..e228c9a1c 100644 --- a/src/components/middle/message/Invoice.scss +++ b/src/components/middle/message/Invoice.scss @@ -48,6 +48,11 @@ .test-invoice { margin-left: 0.5rem; } + + .invoice-currency-icon { + margin-inline-end: 0.125rem; + line-height: 1.5; + } } .invoice-image-container { diff --git a/src/components/middle/message/Invoice.tsx b/src/components/middle/message/Invoice.tsx index baae3449d..7620ea398 100644 --- a/src/components/middle/message/Invoice.tsx +++ b/src/components/middle/message/Invoice.tsx @@ -119,7 +119,7 @@ const Invoice: FC = ({
)}

- {formatCurrency(amount, currency, lang.code)} + {formatCurrency(amount, currency, lang.code, { iconClassName: 'invoice-currency-icon' })} {isTest && {lang('PaymentTestInvoice')}}

diff --git a/src/components/middle/message/InvoiceMediaPreview.tsx b/src/components/middle/message/InvoiceMediaPreview.tsx index 305917951..2dd453c97 100644 --- a/src/components/middle/message/InvoiceMediaPreview.tsx +++ b/src/components/middle/message/InvoiceMediaPreview.tsx @@ -7,7 +7,7 @@ import type { ApiMessage } from '../../../api/types'; import { getMessageInvoice } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; import { formatMediaDuration } from '../../../util/dates/dateFormat'; -import { formatCurrency } from '../../../util/formatCurrency'; +import { formatCurrencyAsString } from '../../../util/formatCurrency'; import useInterval from '../../../hooks/schedulers/useInterval'; import useLastCallback from '../../../hooks/useLastCallback'; @@ -74,7 +74,7 @@ const InvoiceMediaPreview: FC = ({ {Boolean(duration) &&
{formatMediaDuration(duration)}
}
- {lang('Checkout.PayPrice', formatCurrency(amount, currency))} + {lang('Checkout.PayPrice', formatCurrencyAsString(amount, currency))}
); diff --git a/src/components/modals/collectible/CollectibleInfoModal.tsx b/src/components/modals/collectible/CollectibleInfoModal.tsx index ea9a17f59..e1f0bc8c3 100644 --- a/src/components/modals/collectible/CollectibleInfoModal.tsx +++ b/src/components/modals/collectible/CollectibleInfoModal.tsx @@ -10,7 +10,7 @@ import type { TabState } from '../../../global/types'; import { copyTextToClipboard } from '../../../util/clipboard'; import { formatDateAtTime } from '../../../util/dates/dateFormat'; -import { formatCurrency } from '../../../util/formatCurrency'; +import { formatCurrencyAsString } from '../../../util/formatCurrency'; import { formatPhoneNumberWithCode } from '../../../util/phoneNumber'; import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets'; import formatUsername from '../../common/helpers/formatUsername'; @@ -91,8 +91,8 @@ const CollectibleInfoModal: FC = ({ 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 currency = formatCurrencyAsString(modal.amount, modal.currency, lang.code); + const cryptoCurrency = formatCurrencyAsString(modal.cryptoAmount, modal.cryptoCurrency, lang.code); const paid = `${cryptoCurrency} (${currency})`; return lang(key, [date, paid]); }, [modal, isUsername, lang]); diff --git a/src/components/payment/Checkout.tsx b/src/components/payment/Checkout.tsx index 1e2e16bd7..ee3a91595 100644 --- a/src/components/payment/Checkout.tsx +++ b/src/components/payment/Checkout.tsx @@ -121,7 +121,7 @@ const Checkout: FC = ({ className={buildClassName(styles.tipsItem, tip === tipAmount && styles.tipsItem_active)} onClick={dispatch ? () => handleTipsClick(tip === tipAmount ? 0 : tip) : undefined} > - {formatCurrency(tip, currency, lang.code, true)} + {formatCurrency(tip, currency, lang.code, { shouldOmitFractions: true })} ))} @@ -199,7 +199,7 @@ const Checkout: FC = ({ label: lang('PaymentCheckoutProvider'), customIcon: buildClassName(styles.provider, styles[paymentProvider.toLowerCase()]), })} - {(needAddress || !isInteractive) && renderCheckoutItem({ + {(needAddress || (!isInteractive && shippingAddress)) && renderCheckoutItem({ title: shippingAddress, label: lang('PaymentShippingAddress'), icon: 'location', @@ -215,7 +215,7 @@ const Checkout: FC = ({ label: lang('PaymentCheckoutPhoneNumber'), icon: 'phone', })} - {(hasShippingOptions || !isInteractive) && renderCheckoutItem({ + {(hasShippingOptions || (!isInteractive && shippingMethod)) && renderCheckoutItem({ title: shippingMethod, label: lang('PaymentCheckoutShippingMethod'), icon: 'truck', diff --git a/src/components/payment/PaymentModal.tsx b/src/components/payment/PaymentModal.tsx index 559bfed27..993a05c19 100644 --- a/src/components/payment/PaymentModal.tsx +++ b/src/components/payment/PaymentModal.tsx @@ -15,7 +15,7 @@ import { getUserFullName } from '../../global/helpers'; import { selectChat, selectTabState, selectUser } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import captureKeyboardListeners from '../../util/captureKeyboardListeners'; -import { formatCurrency } from '../../util/formatCurrency'; +import { formatCurrencyAsString } from '../../util/formatCurrency'; import { detectCardTypeText } from '../common/helpers/detectCardType'; import usePaymentReducer from '../../hooks/reducers/usePaymentReducer'; @@ -517,7 +517,7 @@ const PaymentModal: FC = ({ }, [step, lang]); const buttonText = step === PaymentStep.Checkout - ? lang('Checkout.PayPrice', formatCurrency(totalPrice, currency!, lang.code)) + ? lang('Checkout.PayPrice', formatCurrencyAsString(totalPrice, currency!, lang.code)) : lang('Next'); function getIsSubmitDisabled() { diff --git a/src/components/ui/Radio.tsx b/src/components/ui/Radio.tsx index 939041fdb..d0fddd2c1 100644 --- a/src/components/ui/Radio.tsx +++ b/src/components/ui/Radio.tsx @@ -14,7 +14,7 @@ type OwnProps = { id?: string; name: string; label: TeactNode; - subLabel?: string; + subLabel?: TeactNode; value: string; checked: boolean; disabled?: boolean; diff --git a/src/components/ui/RadioGroup.tsx b/src/components/ui/RadioGroup.tsx index 416b7e864..9dcbb3c56 100644 --- a/src/components/ui/RadioGroup.tsx +++ b/src/components/ui/RadioGroup.tsx @@ -8,7 +8,7 @@ import Radio from './Radio'; export type IRadioOption = { label: TeactNode; - subLabel?: string; + subLabel?: TeactNode; value: T; hidden?: boolean; className?: string; @@ -25,7 +25,7 @@ type OwnProps = { onClickAction?: (value: string) => void; isLink?: boolean; subLabelClassName?: string; - subLabel?: string | undefined; + subLabel?: TeactNode; }; const RadioGroup: FC = ({ diff --git a/src/global/actions/apiUpdaters/payments.ts b/src/global/actions/apiUpdaters/payments.ts index dc45058e8..55f09bd31 100644 --- a/src/global/actions/apiUpdaters/payments.ts +++ b/src/global/actions/apiUpdaters/payments.ts @@ -1,7 +1,7 @@ import type { ActionReturnType } from '../../types'; import { areDeepEqual } from '../../../util/areDeepEqual'; -import { formatCurrency } from '../../../util/formatCurrency'; +import { formatCurrencyAsString } from '../../../util/formatCurrency'; import * as langProvider from '../../../util/oldLangProvider'; import { addActionHandler, setGlobal } from '../../index'; import { closeInvoice, updateStarsBalance } from '../../reducers'; @@ -22,7 +22,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { actions.showNotification({ tabId, message: langProvider.oldTranslate('PaymentInfoHint', [ - formatCurrency(amount, currency, langProvider.getTranslationFn().code), + formatCurrencyAsString(amount, currency, langProvider.getTranslationFn().code), title, ]), }); diff --git a/src/hooks/useOldLangString.ts b/src/hooks/useOldLangString.ts index f9f7eb2f8..ed145e13c 100644 --- a/src/hooks/useOldLangString.ts +++ b/src/hooks/useOldLangString.ts @@ -1,6 +1,9 @@ import * as langProvider from '../util/oldLangProvider'; import useAsync from './useAsync'; +/** + * @deprecated Migrate to `useLang`, while using needed key inside initial fallback + */ const useOldLangString = ( langCode: string | undefined, key: string, diff --git a/src/styles/index.scss b/src/styles/index.scss index 0e71b8817..8a2c96024 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -284,6 +284,11 @@ body:not(.is-ios) { left: 0; } +.star-currency-icon { + font-size: 1rem !important; + vertical-align: text-top; +} + .shared-canvas-container { position: relative; } diff --git a/src/util/formatCurrency.ts b/src/util/formatCurrency.tsx similarity index 61% rename from src/util/formatCurrency.ts rename to src/util/formatCurrency.tsx index d859bafa6..5762ce16f 100644 --- a/src/util/formatCurrency.ts +++ b/src/util/formatCurrency.tsx @@ -1,20 +1,40 @@ +import React, { type TeactNode } from '../lib/teact/teact'; + import type { LangCode } from '../types'; +import StarIcon from '../components/common/icons/StarIcon'; + const STARS_CODE = 'XTR'; export function formatCurrency( totalPrice: number, currency: string, locale: LangCode = 'en', - shouldOmitFractions = false, -) { + options?: { + shouldOmitFractions?: boolean; + iconClassName?: string; + }, +): TeactNode { const price = totalPrice / 10 ** getCurrencyExp(currency); if (currency === STARS_CODE) { - return `⭐️${price}`; + return [, price]; } - if (shouldOmitFractions && price % 1 === 0) { + return formatCurrencyAsString(totalPrice, currency, locale, options); +} + +export function formatCurrencyAsString( + totalPrice: number, + currency: string, + locale: LangCode = 'en', + options?: { + shouldOmitFractions?: boolean; + }, +) { + const price = totalPrice / 10 ** getCurrencyExp(currency); + + if (options?.shouldOmitFractions && price % 1 === 0) { return new Intl.NumberFormat(locale, { style: 'currency', currency,