335 lines
11 KiB
TypeScript
335 lines
11 KiB
TypeScript
import {
|
|
memo, useEffect, useState,
|
|
} from '../../../lib/teact/teact';
|
|
import { getActions, withGlobal } from '../../../global';
|
|
|
|
import type { ApiDraft, ApiPeer, ApiStarsAmount, ApiTypeCurrencyAmount } from '../../../api/types';
|
|
import type { TabState } from '../../../global/types';
|
|
import { MAIN_THREAD_ID } from '../../../api/types';
|
|
|
|
import {
|
|
STARS_CURRENCY_CODE,
|
|
TON_CURRENCY_CODE,
|
|
} from '../../../config';
|
|
import { selectIsMonoforumAdmin, selectPeer } from '../../../global/selectors';
|
|
import { selectDraft } from '../../../global/selectors/threads';
|
|
import buildClassName from '../../../util/buildClassName';
|
|
import { formatScheduledDateTime, formatShortDuration } from '../../../util/dates/oldDateFormat';
|
|
import { convertTonFromNanos, convertTonToNanos } from '../../../util/formatCurrency';
|
|
import {
|
|
formatStarsAsIcon,
|
|
formatStarsAsText,
|
|
formatTonAsIcon,
|
|
formatTonAsText } from '../../../util/localization/format';
|
|
import { getServerTime } from '../../../util/serverTime';
|
|
|
|
import useFlag from '../../../hooks/useFlag';
|
|
import useLang from '../../../hooks/useLang';
|
|
import useLastCallback from '../../../hooks/useLastCallback';
|
|
import useOldLang from '../../../hooks/useOldLang';
|
|
|
|
import CalendarModal from '../../common/CalendarModal';
|
|
import Icon from '../../common/icons/Icon';
|
|
import Button from '../../ui/Button';
|
|
import InputText from '../../ui/InputText';
|
|
import Modal from '../../ui/Modal';
|
|
|
|
import styles from './SuggestMessageModal.module.scss';
|
|
|
|
export type OwnProps = {
|
|
modal: TabState['suggestMessageModal'];
|
|
};
|
|
|
|
type StateProps = {
|
|
starBalance?: ApiStarsAmount;
|
|
tonBalance?: number;
|
|
peer?: ApiPeer;
|
|
currentDraft?: ApiDraft;
|
|
maxStarsAmount: number;
|
|
minStarsAmount: number;
|
|
tonMaxAmount: number;
|
|
tonMinAmount: number;
|
|
ageMinSeconds: number;
|
|
futureMin: number;
|
|
futureMax: number;
|
|
isMonoforumAdmin?: boolean;
|
|
};
|
|
|
|
// Add 1 minute if time is less than server min, to allow user to send the message
|
|
const FUTURE_TIME_ADJUSTMENT = 1 * 60;
|
|
|
|
const SuggestMessageModal = ({
|
|
modal,
|
|
starBalance,
|
|
tonBalance,
|
|
peer,
|
|
currentDraft,
|
|
maxStarsAmount,
|
|
minStarsAmount,
|
|
tonMaxAmount,
|
|
tonMinAmount,
|
|
ageMinSeconds,
|
|
futureMin,
|
|
futureMax,
|
|
isMonoforumAdmin,
|
|
}: OwnProps & StateProps) => {
|
|
const { closeSuggestMessageModal, updateDraftSuggestedPostInfo, openStarsBalanceModal } = getActions();
|
|
const [isCalendarOpened, openCalendar, closeCalendar] = useFlag();
|
|
|
|
const currentSuggestedPostInfo = currentDraft?.suggestedPostInfo;
|
|
const currentReplyInfo = currentDraft?.replyInfo;
|
|
const isInSuggestChangesMode = Boolean(currentReplyInfo);
|
|
|
|
const [currencyAmount, setCurrencyAmount] = useState<number | undefined>(
|
|
currentSuggestedPostInfo?.price?.amount || undefined,
|
|
);
|
|
const [selectedCurrency, setSelectedCurrency] = useState<ApiTypeCurrencyAmount['currency']>(
|
|
currentSuggestedPostInfo?.price?.currency || STARS_CURRENCY_CODE,
|
|
);
|
|
const [scheduleDate, setScheduleDate] = useState<number | undefined>(
|
|
currentSuggestedPostInfo?.scheduleDate
|
|
? currentSuggestedPostInfo.scheduleDate * 1000
|
|
: undefined,
|
|
);
|
|
|
|
useEffect(() => {
|
|
const price = currentSuggestedPostInfo?.price;
|
|
const amount = price?.currency === TON_CURRENCY_CODE ? convertTonFromNanos(price.amount) : price?.amount;
|
|
setCurrencyAmount(amount);
|
|
setSelectedCurrency(currentSuggestedPostInfo?.price?.currency || STARS_CURRENCY_CODE);
|
|
setScheduleDate(currentSuggestedPostInfo?.scheduleDate
|
|
? currentSuggestedPostInfo.scheduleDate * 1000
|
|
: undefined);
|
|
}, [currentSuggestedPostInfo]);
|
|
|
|
const lang = useLang();
|
|
const oldLang = useOldLang();
|
|
|
|
const isCurrencyStars = selectedCurrency === STARS_CURRENCY_CODE;
|
|
const now = getServerTime();
|
|
const minAt = (now + futureMin) * 1000;
|
|
const maxAt = (now + futureMax) * 1000;
|
|
const defaultSelectedTime = (now + futureMin * 2) * 1000;
|
|
|
|
const handleAmountChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const value = e.target.value;
|
|
const number = parseFloat(value);
|
|
|
|
const result = value === '' || Number.isNaN(number) ? undefined
|
|
: Math.min(Math.max(number, 0), currentMaxAmount);
|
|
|
|
setCurrencyAmount(result);
|
|
});
|
|
|
|
const handleExpireDateChange = useLastCallback((date: Date) => {
|
|
setScheduleDate(date.getTime());
|
|
closeCalendar();
|
|
});
|
|
|
|
const handleAnytimeClick = useLastCallback(() => {
|
|
setScheduleDate(undefined);
|
|
closeCalendar();
|
|
});
|
|
|
|
const currentMinAmount = isCurrencyStars ? minStarsAmount : convertTonFromNanos(tonMinAmount);
|
|
const currentMaxAmount = isCurrencyStars ? maxStarsAmount : convertTonFromNanos(tonMaxAmount);
|
|
const isDisabled = Boolean(currencyAmount) && currencyAmount < currentMinAmount;
|
|
|
|
const handleOffer = useLastCallback(() => {
|
|
const neededAmount = currencyAmount
|
|
? (isCurrencyStars ? currencyAmount : convertTonToNanos(currencyAmount))
|
|
: 0;
|
|
|
|
if (isDisabled) {
|
|
return;
|
|
}
|
|
|
|
if (!isMonoforumAdmin) {
|
|
if (isCurrencyStars) {
|
|
const currentBalance = starBalance?.amount || 0;
|
|
|
|
if (neededAmount > currentBalance) {
|
|
openStarsBalanceModal({
|
|
topup: {
|
|
balanceNeeded: neededAmount,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
} else {
|
|
const currentTonBalance = tonBalance || 0;
|
|
if (neededAmount > currentTonBalance) {
|
|
openStarsBalanceModal({
|
|
currency: TON_CURRENCY_CODE,
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
updateDraftSuggestedPostInfo({
|
|
price: { currency: selectedCurrency, amount: neededAmount, nanos: 0 },
|
|
scheduleDate: scheduleDate
|
|
? Math.max(scheduleDate / 1000, getServerTime() + futureMin + FUTURE_TIME_ADJUSTMENT)
|
|
: undefined,
|
|
});
|
|
|
|
closeSuggestMessageModal();
|
|
});
|
|
|
|
return (
|
|
<Modal
|
|
headerClassName={styles.modalHeader}
|
|
isOpen={Boolean(modal)}
|
|
onClose={closeSuggestMessageModal}
|
|
isSlim
|
|
isLowStackPriority
|
|
hasCloseButton
|
|
contentClassName={styles.content}
|
|
title={isInSuggestChangesMode ? lang('TitleSuggestedChanges') : lang('TitleSuggestMessage')}
|
|
>
|
|
<div className={styles.form}>
|
|
<div className={styles.section}>
|
|
<div className={styles.currencySelector}>
|
|
<Button
|
|
className={styles.currencyButton}
|
|
color={isCurrencyStars ? 'primary' : 'translucent'}
|
|
pill
|
|
fluid
|
|
size="tiny"
|
|
noFastClick
|
|
onClick={() => setSelectedCurrency(STARS_CURRENCY_CODE)}
|
|
>
|
|
<Icon name="star" className={styles.currencyIcon} />
|
|
{lang('CurrencyStars')}
|
|
</Button>
|
|
<Button
|
|
className={styles.currencyButton}
|
|
fluid
|
|
color={!isCurrencyStars ? 'primary' : 'translucent'}
|
|
pill
|
|
size="tiny"
|
|
noFastClick
|
|
onClick={() => setSelectedCurrency(TON_CURRENCY_CODE)}
|
|
>
|
|
<Icon name="toncoin" className={styles.currencyIcon} />
|
|
{lang('CurrencyTon')}
|
|
</Button>
|
|
</div>
|
|
<InputText
|
|
label={lang('InputPlaceholderPrice')}
|
|
className={buildClassName(styles.input)}
|
|
value={currencyAmount?.toString()}
|
|
onChange={handleAmountChange}
|
|
inputMode="numeric"
|
|
tabIndex={0}
|
|
teactExperimentControlled={isCurrencyStars}
|
|
/>
|
|
<div className={styles.description}>
|
|
{currencyAmount !== undefined && currencyAmount > 0 && currencyAmount < currentMinAmount
|
|
? lang('DescriptionSuggestedPostMinimumOffer', {
|
|
amount: isCurrencyStars
|
|
? formatStarsAsText(lang, currentMinAmount)
|
|
: formatTonAsText(lang, currentMinAmount) },
|
|
{ withNodes: true, withMarkdown: true })
|
|
: isCurrencyStars
|
|
? lang('SuggestMessagePriceDescriptionStars')
|
|
: lang('SuggestMessagePriceDescriptionTon')}
|
|
</div>
|
|
</div>
|
|
|
|
<div className={styles.section}>
|
|
<div className={buildClassName(styles.input, 'input-group', 'touched')}>
|
|
<input
|
|
type="text"
|
|
className={buildClassName('form-control', isCalendarOpened && 'focus')}
|
|
value={scheduleDate
|
|
? formatScheduledDateTime(scheduleDate / 1000, lang, oldLang)
|
|
: lang('SuggestMessageAnytime')}
|
|
autoComplete="off"
|
|
onClick={openCalendar}
|
|
onFocus={openCalendar}
|
|
readOnly
|
|
/>
|
|
<label>{lang('InputTitleSuggestMessageTime')}</label>
|
|
<Icon name="down" className={styles.timeInputIcon} />
|
|
</div>
|
|
<div className={styles.description}>
|
|
{lang('SuggestMessageTimeDescription', {
|
|
hint: lang('SuggestMessageDateTimeHint'),
|
|
duration: formatShortDuration(lang, ageMinSeconds, true),
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<CalendarModal
|
|
isOpen={isCalendarOpened}
|
|
isFutureMode
|
|
withTimePicker
|
|
minAt={minAt}
|
|
maxAt={maxAt}
|
|
onClose={closeCalendar}
|
|
onSubmit={handleExpireDateChange}
|
|
selectedAt={scheduleDate || defaultSelectedTime}
|
|
submitButtonLabel={lang('Save')}
|
|
secondButtonLabel={lang('SuggestMessageAnytime')}
|
|
onSecondButtonClick={handleAnytimeClick}
|
|
description={lang('SuggestMessageDateTimeHint')}
|
|
/>
|
|
|
|
<Button
|
|
className={styles.offerButton}
|
|
onClick={handleOffer}
|
|
disabled={isDisabled}
|
|
inline
|
|
>
|
|
{isInSuggestChangesMode ? lang('ButtonUpdateTerms')
|
|
: currencyAmount ? lang('ButtonOfferAmount', {
|
|
amount: isCurrencyStars
|
|
? formatStarsAsIcon(lang, currencyAmount, { asFont: true })
|
|
: formatTonAsIcon(lang, currencyAmount),
|
|
}, {
|
|
withNodes: true,
|
|
withMarkdown: true,
|
|
}) : lang('ButtonOfferFree')}
|
|
</Button>
|
|
</div>
|
|
</Modal>
|
|
);
|
|
};
|
|
|
|
export default memo(withGlobal<OwnProps>(
|
|
(global, { modal }): Complete<StateProps> => {
|
|
const starBalance = global.stars?.balance;
|
|
const peer = modal ? selectPeer(global, modal.chatId) : undefined;
|
|
const currentDraft = modal ? selectDraft(global, modal.chatId, MAIN_THREAD_ID) : undefined;
|
|
|
|
const { appConfig } = global;
|
|
const maxStarsAmount = appConfig.starsSuggestedPostAmountMax;
|
|
const minStarsAmount = appConfig.starsSuggestedPostAmountMin;
|
|
const ageMinSeconds = appConfig.starsSuggestedPostAgeMin;
|
|
const futureMin = appConfig.starsSuggestedPostFutureMin;
|
|
const futureMax = appConfig.starsSuggestedPostFutureMax;
|
|
|
|
const tonMaxAmount = appConfig.tonSuggestedPostAmountMax;
|
|
const tonMinAmount = appConfig.tonSuggestedPostAmountMin;
|
|
|
|
const isMonoforumAdmin = modal ? selectIsMonoforumAdmin(global, modal.chatId) : false;
|
|
|
|
return {
|
|
peer,
|
|
starBalance,
|
|
tonBalance: global.ton?.balance?.amount,
|
|
currentDraft,
|
|
maxStarsAmount,
|
|
minStarsAmount,
|
|
tonMaxAmount,
|
|
tonMinAmount,
|
|
ageMinSeconds,
|
|
futureMin,
|
|
futureMax,
|
|
isMonoforumAdmin,
|
|
};
|
|
},
|
|
)(SuggestMessageModal));
|