Stars: Format currency with icon (#4678)
This commit is contained in:
parent
41cc393a3c
commit
6f79159ec3
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -726,7 +726,6 @@ interface ApiKeyboardButtonSimple {
|
||||
|
||||
interface ApiKeyboardButtonReceipt {
|
||||
type: 'receipt';
|
||||
text: string;
|
||||
receiptMessageId: number;
|
||||
}
|
||||
|
||||
|
||||
@ -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})`;
|
||||
}
|
||||
|
||||
|
||||
@ -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%;
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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<OwnProps> = ({
|
||||
onMouseEnter={!IS_TOUCH_ENV ? markNoHoverColor : undefined}
|
||||
onMouseLeave={!IS_TOUCH_ENV ? unmarkNoHoverColor : undefined}
|
||||
>
|
||||
{renderText(inlineButton.text)}
|
||||
{renderKeyboardButtonText(lang, inlineButton)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -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<OwnProps & StateProps> = ({
|
||||
}) => {
|
||||
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<OwnProps & StateProps> = ({
|
||||
noCompact
|
||||
>
|
||||
<div className="content custom-scroll">
|
||||
{message.keyboardButtons.map((row) => (
|
||||
{message.keyboardButtons.map((row, i) => (
|
||||
<div className="row">
|
||||
{row.map((button) => (
|
||||
{row.map((button, j) => (
|
||||
<Button
|
||||
ripple
|
||||
disabled={button.type === 'unsupported'}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => clickBotInlineButton({ messageId: message.id, button })}
|
||||
>
|
||||
{button.text}
|
||||
{buttonTexts?.[i][j]}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -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, '⭐', <Icon className="star-currency-icon" name="star" />);
|
||||
}
|
||||
|
||||
return renderText(button.text);
|
||||
}
|
||||
@ -37,7 +37,7 @@
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
.corner-icon {
|
||||
font-size: 0.875rem;
|
||||
position: absolute;
|
||||
right: 0.1875rem;
|
||||
|
||||
@ -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<OwnProps> = ({ message, onClick }) => {
|
||||
switch (type) {
|
||||
case 'url': {
|
||||
if (!RE_TME_LINK.test(button.url)) {
|
||||
return <i className="icon icon-arrow-right" />;
|
||||
return <Icon className="corner-icon" name="arrow-right" />;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'urlAuth':
|
||||
return <i className="icon icon-arrow-right" />;
|
||||
return <Icon className="corner-icon" name="arrow-right" />;
|
||||
case 'buy':
|
||||
case 'receipt':
|
||||
return <i className="icon icon-cart" />;
|
||||
return <Icon className="corner-icon" name="card" />;
|
||||
case 'switchBotInline':
|
||||
return <i className="icon icon-share-filled" />;
|
||||
return <Icon className="corner-icon" name="share-filled" />;
|
||||
case 'webView':
|
||||
case 'simpleWebView':
|
||||
return <i className="icon icon-webapp" />;
|
||||
return <Icon className="corner-icon" name="webapp" />;
|
||||
}
|
||||
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 (
|
||||
<div className="InlineButtons">
|
||||
{message.inlineButtons!.map((row) => (
|
||||
{message.inlineButtons!.map((row, i) => (
|
||||
<div className="row">
|
||||
{row.map((button) => (
|
||||
{row.map((button, j) => (
|
||||
<Button
|
||||
size="tiny"
|
||||
ripple
|
||||
@ -55,7 +64,9 @@ const InlineButtons: FC<OwnProps> = ({ message, onClick }) => {
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => onClick({ messageId: message.id, button })}
|
||||
>
|
||||
<span className="inline-button-text">{renderText(lang(button.text))}</span>
|
||||
<span className="inline-button-text">
|
||||
{buttonTexts[i][j]}
|
||||
</span>
|
||||
{renderIcon(button)}
|
||||
</Button>
|
||||
))}
|
||||
@ -65,4 +76,4 @@ const InlineButtons: FC<OwnProps> = ({ message, onClick }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default InlineButtons;
|
||||
export default memo(InlineButtons);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -119,7 +119,7 @@ const Invoice: FC<OwnProps> = ({
|
||||
</div>
|
||||
)}
|
||||
<p className="description-text">
|
||||
{formatCurrency(amount, currency, lang.code)}
|
||||
{formatCurrency(amount, currency, lang.code, { iconClassName: 'invoice-currency-icon' })}
|
||||
{isTest && <span className="test-invoice">{lang('PaymentTestInvoice')}</span>}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -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<OwnProps> = ({
|
||||
{Boolean(duration) && <div className={styles.duration}>{formatMediaDuration(duration)}</div>}
|
||||
<div className={styles.buy}>
|
||||
<i className={buildClassName('icon', 'icon-lock', styles.lock)} />
|
||||
{lang('Checkout.PayPrice', formatCurrency(amount, currency))}
|
||||
{lang('Checkout.PayPrice', formatCurrencyAsString(amount, currency))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -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<OwnProps & StateProps> = ({
|
||||
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]);
|
||||
|
||||
@ -121,7 +121,7 @@ const Checkout: FC<OwnProps> = ({
|
||||
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 })}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -199,7 +199,7 @@ const Checkout: FC<OwnProps> = ({
|
||||
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<OwnProps> = ({
|
||||
label: lang('PaymentCheckoutPhoneNumber'),
|
||||
icon: 'phone',
|
||||
})}
|
||||
{(hasShippingOptions || !isInteractive) && renderCheckoutItem({
|
||||
{(hasShippingOptions || (!isInteractive && shippingMethod)) && renderCheckoutItem({
|
||||
title: shippingMethod,
|
||||
label: lang('PaymentCheckoutShippingMethod'),
|
||||
icon: 'truck',
|
||||
|
||||
@ -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<OwnProps & StateProps & GlobalStateProps> = ({
|
||||
}, [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() {
|
||||
|
||||
@ -14,7 +14,7 @@ type OwnProps = {
|
||||
id?: string;
|
||||
name: string;
|
||||
label: TeactNode;
|
||||
subLabel?: string;
|
||||
subLabel?: TeactNode;
|
||||
value: string;
|
||||
checked: boolean;
|
||||
disabled?: boolean;
|
||||
|
||||
@ -8,7 +8,7 @@ import Radio from './Radio';
|
||||
|
||||
export type IRadioOption<T = string> = {
|
||||
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<OwnProps> = ({
|
||||
|
||||
@ -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,
|
||||
]),
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 [<StarIcon className={options?.iconClassName} type="gold" size="adaptive" />, 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,
|
||||
Loading…
x
Reference in New Issue
Block a user