feat: debounce input sliders (#784)

* feat: debounce input sliders

* fix: fixed the debounce function
This commit is contained in:
Linkie Link 2024-02-09 08:29:26 +01:00 committed by GitHub
parent 6d5e7c7325
commit 6ac7708ca5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 172 additions and 98 deletions

View File

@ -24,6 +24,7 @@ interface Props {
onClose: () => void
onChange: (value: BigNumber) => void
onAction: (value: BigNumber, isMax: boolean) => void
onDebounce: () => void
}
export default function AssetAmountSelectActionModal(props: Props) {
@ -36,6 +37,7 @@ export default function AssetAmountSelectActionModal(props: Props) {
onClose,
onChange,
onAction,
onDebounce,
} = props
const [amount, setAmount] = useState(BN_ZERO)
const maxAmount = BN(coinBalances.find(byDenom(asset.denom))?.amount ?? 0)
@ -74,6 +76,7 @@ export default function AssetAmountSelectActionModal(props: Props) {
<TokenInputWithSlider
asset={asset}
onChange={handleAmountChange}
onDebounce={onDebounce}
amount={amount}
max={maxAmount}
hasSelect

View File

@ -1,8 +1,8 @@
import BigNumber from 'bignumber.js'
import { useCallback, useEffect, useMemo, useState } from 'react'
import AccountSummary from 'components/account/AccountSummary'
import AssetImage from 'components/common/assets/AssetImage'
import Modal from 'components/Modals/Modal'
import AccountSummaryInModal from 'components/account/AccountSummary/AccountSummaryInModal'
import Button from 'components/common/Button'
import Card from 'components/common/Card'
import DisplayCurrency from 'components/common/DisplayCurrency'
@ -13,7 +13,7 @@ import Switch from 'components/common/Switch'
import Text from 'components/common/Text'
import TitleAndSubCell from 'components/common/TitleAndSubCell'
import TokenInputWithSlider from 'components/common/TokenInput/TokenInputWithSlider'
import Modal from 'components/Modals/Modal'
import AssetImage from 'components/common/assets/AssetImage'
import { BN_ZERO } from 'constants/math'
import useCurrentAccount from 'hooks/accounts/useCurrentAccount'
import useMarkets from 'hooks/markets/useMarkets'
@ -29,7 +29,6 @@ import { byDenom } from 'utils/array'
import { formatPercent } from 'utils/formatters'
import { BN } from 'utils/helpers'
import { getDebtAmountWithInterest } from 'utils/tokens'
import AccountSummaryInModal from 'components/account/AccountSummary/AccountSummaryInModal'
interface Props {
account: Account
@ -92,6 +91,7 @@ function BorrowModal(props: Props) {
const isAutoLendEnabled = autoLendEnabledAccountIds.includes(account.id)
const { computeMaxBorrowAmount } = useHealthComputer(account)
const totalDebt = BN(getDebtAmount(modal))
const accountDebt = account.debts.find(byDenom(asset.denom))?.amount ?? BN_ZERO
const markets = useMarkets()
const [depositBalance, lendBalance] = useMemo(
@ -102,34 +102,34 @@ function BorrowModal(props: Props) {
[account, asset.denom],
)
const totalDebtRepayAmount = useMemo(
() => getDebtAmountWithInterest(totalDebt, apy),
[totalDebt, apy],
const accountDebtWithInterest = useMemo(
() => getDebtAmountWithInterest(accountDebt, apy),
[accountDebt, apy],
)
const overpayExeedsCap = useMemo(() => {
const marketAsset = markets.find((market) => market.asset.denom === asset.denom)
if (!marketAsset) return
const overpayAmount = totalDebtRepayAmount.minus(totalDebt)
const overpayAmount = accountDebtWithInterest.minus(accountDebt)
const marketCapAfterOverpay = marketAsset.cap.used.plus(overpayAmount)
return marketAsset.cap.max.isLessThanOrEqualTo(marketCapAfterOverpay)
}, [markets, asset.denom, totalDebt, totalDebtRepayAmount])
}, [markets, asset.denom, accountDebt, accountDebtWithInterest])
const maxRepayAmount = useMemo(() => {
const maxBalance = repayFromWallet
? BN(walletBalances.find(byDenom(asset.denom))?.amount ?? 0)
: depositBalance.plus(lendBalance)
return isRepay
? BigNumber.min(maxBalance, overpayExeedsCap ? totalDebt : totalDebtRepayAmount)
? BigNumber.min(maxBalance, overpayExeedsCap ? accountDebt : accountDebtWithInterest)
: BN_ZERO
}, [
depositBalance,
lendBalance,
isRepay,
totalDebtRepayAmount,
accountDebtWithInterest,
overpayExeedsCap,
totalDebt,
accountDebt,
walletBalances,
asset.denom,
repayFromWallet,
@ -149,7 +149,7 @@ function BorrowModal(props: Props) {
repay({
accountId: account.id,
coin: BNCoin.fromDenomAndBigNumber(asset.denom, amount),
accountBalance: amount.isEqualTo(totalDebtRepayAmount),
accountBalance: amount.isEqualTo(accountDebtWithInterest),
lend: repayFromWallet ? BNCoin.fromDenomAndBigNumber(asset.denom, BN_ZERO) : lend,
fromWallet: repayFromWallet,
})
@ -172,17 +172,39 @@ function BorrowModal(props: Props) {
const handleChange = useCallback(
(newAmount: BigNumber) => {
const coin = BNCoin.fromDenomAndBigNumber(asset.denom, newAmount)
if (!amount.isEqualTo(newAmount)) setAmount(newAmount)
if (!isRepay) return
const repayCoin = coin.amount.isGreaterThan(totalDebt)
? BNCoin.fromDenomAndBigNumber(asset.denom, totalDebt)
: coin
simulateRepay(repayCoin, repayFromWallet)
},
[amount, asset.denom, isRepay, simulateRepay, totalDebt, repayFromWallet],
[amount, setAmount],
)
const onDebounce = useCallback(() => {
if (isRepay) {
const repayCoin = BNCoin.fromDenomAndBigNumber(
asset.denom,
amount.isGreaterThan(accountDebt) ? accountDebt : amount,
)
simulateRepay(repayCoin, repayFromWallet)
} else {
const borrowCoin = BNCoin.fromDenomAndBigNumber(
asset.denom,
amount.isGreaterThan(max) ? max : amount,
)
const target = borrowToWallet ? 'wallet' : isAutoLendEnabled ? 'lend' : 'deposit'
simulateBorrow(target, borrowCoin)
}
}, [
amount,
isRepay,
repayFromWallet,
maxRepayAmount,
max,
asset,
borrowToWallet,
isAutoLendEnabled,
simulateBorrow,
simulateRepay,
])
const maxBorrow = useMemo(() => {
const maxBorrowAmount = isRepay
? BN_ZERO
@ -209,13 +231,6 @@ function BorrowModal(props: Props) {
setAmount(max)
}, [amount, max, handleChange])
useEffect(() => {
if (isRepay) return
const coin = BNCoin.fromDenomAndBigNumber(asset.denom, amount.isGreaterThan(max) ? max : amount)
const target = borrowToWallet ? 'wallet' : isAutoLendEnabled ? 'lend' : 'deposit'
simulateBorrow(target, coin)
}, [isRepay, borrowToWallet, isAutoLendEnabled, simulateBorrow, asset, amount, max])
if (!modal || !asset) return null
return (
<Modal
@ -257,7 +272,7 @@ function BorrowModal(props: Props) {
/>
</div>
<Text size='xs' className='text-white/50' tag='span'>
Borrowed
Total Borrowed
</Text>
</div>
</>
@ -294,6 +309,7 @@ function BorrowModal(props: Props) {
<TokenInputWithSlider
asset={asset}
onChange={handleChange}
onDebounce={onDebounce}
amount={amount}
max={max}
disabled={max.isZero()}

View File

@ -1,5 +1,5 @@
import BigNumber from 'bignumber.js'
import { useEffect, useState } from 'react'
import { useCallback, useState } from 'react'
import Button from 'components/common/Button'
import Divider from 'components/common/Divider'
@ -86,17 +86,10 @@ export default function WithdrawFromAccount(props: Props) {
useStore.setState({ fundAndWithdrawModal: null })
}
useEffect(() => {
const onDebounce = useCallback(() => {
const coin = BNCoin.fromDenomAndBigNumber(currentAsset.denom, withdrawAmount.plus(debtAmount))
simulateWithdraw(withdrawWithBorrowing, coin)
}, [
amount,
withdrawWithBorrowing,
currentAsset.denom,
debtAmount,
simulateWithdraw,
withdrawAmount,
])
}, [withdrawWithBorrowing, currentAsset.denom, debtAmount, simulateWithdraw, withdrawAmount])
return (
<>
@ -104,6 +97,7 @@ export default function WithdrawFromAccount(props: Props) {
<TokenInputWithSlider
asset={currentAsset}
onChange={onChangeAmount}
onDebounce={onDebounce}
onChangeAsset={(asset) => {
setAmount(BN_ZERO)
setWithdrawWithBorrowing(false)

View File

@ -1,4 +1,4 @@
import { useCallback } from 'react'
import { useCallback, useState } from 'react'
import AssetAmountSelectActionModal from 'components/Modals/AssetAmountSelectActionModal'
import DetailsHeader from 'components/Modals/LendAndReclaim/DetailsHeader'
@ -27,6 +27,7 @@ function LendAndReclaimModal({ currentAccount, config }: Props) {
const reclaim = useStore((s) => s.reclaim)
const { close } = useLendAndReclaimModal()
const { simulateLending } = useUpdatedAccount(currentAccount)
const [coin, setCoin] = useState<BNCoin>()
const { data, action } = config
const { asset } = data
@ -37,12 +38,16 @@ function LendAndReclaimModal({ currentAccount, config }: Props) {
const handleAmountChange = useCallback(
(value: BigNumber) => {
const coin = BNCoin.fromDenomAndBigNumber(asset.denom, value)
simulateLending(isLendAction, coin)
setCoin(BNCoin.fromDenomAndBigNumber(asset.denom, value))
},
[asset.denom, isLendAction, simulateLending],
[asset.denom],
)
const onDebounce = useCallback(() => {
if (!coin) return
simulateLending(isLendAction, coin)
}, [coin, isLendAction, simulateLending])
const handleAction = useCallback(
(value: BigNumber, isMax: boolean) => {
const coin = BNCoin.fromDenomAndBigNumber(asset.denom, value)
@ -70,6 +75,7 @@ function LendAndReclaimModal({ currentAccount, config }: Props) {
onClose={close}
onAction={handleAction}
onChange={handleAmountChange}
onDebounce={onDebounce}
/>
)
}

View File

@ -95,10 +95,6 @@ export default function AccountFundContent(props: Props) {
}
}, [baseBalance])
useEffect(() => {
simulateDeposits(isLending ? 'lend' : 'deposit', fundingAssets)
}, [isLending, fundingAssets, simulateDeposits])
useEffect(() => {
const currentSelectedDenom = fundingAssets.map((asset) => asset.denom)
@ -125,6 +121,10 @@ export default function AccountFundContent(props: Props) {
})
}, [])
const onDebounce = useCallback(() => {
simulateDeposits(isLending ? 'lend' : 'deposit', fundingAssets)
}, [isLending, fundingAssets, simulateDeposits])
const depositCapReachedCoins = useMemo(() => {
const depositCapReachedCoins: BNCoin[] = []
fundingAssets.forEach((asset) => {
@ -159,6 +159,7 @@ export default function AccountFundContent(props: Props) {
amount={coin.amount ?? BN_ZERO}
isConfirming={isConfirming}
updateFundingAssets={updateFundingAssets}
onDebounce={onDebounce}
/>
</div>
)

View File

@ -10,6 +10,7 @@ interface Props {
denom: string
isConfirming: boolean
updateFundingAssets: (amount: BigNumber, denom: string) => void
onDebounce: () => void
}
export default function AccountFundRow(props: Props) {
@ -23,6 +24,7 @@ export default function AccountFundRow(props: Props) {
<TokenInputWithSlider
asset={asset}
onChange={(amount) => props.updateFundingAssets(amount, asset.denom)}
onDebounce={props.onDebounce}
amount={props.amount}
max={balance}
balances={props.balances}

View File

@ -11,7 +11,7 @@ interface Props {
}
export default function BorrowActionButtons(props: Props) {
const { asset, debt } = props.data
const { asset, accountDebt } = props.data
const marketAssets = useMarketEnabledAssets()
const currentAsset = marketAssets.find((a) => a.denom === asset.denom)
@ -33,10 +33,10 @@ export default function BorrowActionButtons(props: Props) {
leftIcon={<Plus className='w-3' />}
onClick={borrowHandler}
color='secondary'
text={debt ? 'Borrow more' : 'Borrow'}
className='min-w-40 text-center'
text={accountDebt ? 'Borrow more' : 'Borrow'}
className='text-center min-w-40'
/>
{debt && (
{accountDebt && (
<Button color='tertiary' leftIcon={<HandCoins />} text='Repay' onClick={repayHandler} />
)}
</div>

View File

@ -1,5 +1,6 @@
import classNames from 'classnames'
import { ChangeEvent, useCallback } from 'react'
import debounce from 'lodash.debounce'
import { ChangeEvent, useCallback, useMemo } from 'react'
import InputOverlay from 'components/common/LeverageSlider/InputOverlay'
@ -13,20 +14,41 @@ type Props = {
marginThreshold?: number
wrapperClassName?: string
onChange: (value: number) => void
onDebounce?: () => void
onBlur?: () => void
type: LeverageSliderType
}
export type LeverageSliderType = 'margin' | 'long' | 'short'
function LeverageSlider(props: Props) {
const { value, max, onChange, wrapperClassName, disabled, marginThreshold, onBlur, type } = props
const {
value,
max,
onChange,
wrapperClassName,
disabled,
marginThreshold,
onBlur,
type,
onDebounce,
} = props
const min = props.min ?? 0
const debounceFunction = useMemo(
() =>
debounce(() => {
if (!onDebounce) return
onDebounce()
}, 250),
[onDebounce],
)
const handleOnChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
onChange(parseFloat(event.target.value))
debounceFunction()
},
[onChange],
[onChange, debounceFunction],
)
const markPosPercent = 100 / (max / (marginThreshold ?? 1))

View File

@ -1,4 +1,5 @@
import classNames from 'classnames'
import debounce from 'lodash.debounce'
import { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import Draggable from 'react-draggable'
@ -19,6 +20,7 @@ const colors = {
type Props = {
value: number
onChange: (value: number) => void
onDebounce?: () => void
leverage?: {
current: number
max: number
@ -28,6 +30,7 @@ type Props = {
}
export default function Slider(props: Props) {
const { value, onChange, onDebounce, leverage, className, disabled } = props
const [showTooltip, setShowTooltip] = useToggle()
const [sliderRect, setSliderRect] = useState({ width: 0, left: 0, right: 0 })
const ref = useRef<HTMLDivElement>(null)
@ -52,6 +55,20 @@ export default function Slider(props: Props) {
}
}, [sliderRect.left, sliderRect.right, sliderRect.width])
const debounceFunction = useMemo(
() =>
debounce(() => {
if (!onDebounce) return
onDebounce()
}, 250),
[onDebounce],
)
function handleOnChange(value: number) {
onChange(value)
debounceFunction()
}
function handleDrag(e: any) {
if (!isDragging) {
setIsDragging(true)
@ -60,24 +77,24 @@ export default function Slider(props: Props) {
const current: number = e.clientX
if (current < sliderRect.left) {
props.onChange(0)
handleOnChange(0)
return
}
if (current > sliderRect.right) {
props.onChange(100)
handleOnChange(100)
return
}
const value = Math.round(((current - sliderRect.left) / sliderRect.width) * 100)
const currentValue = Math.round(((current - sliderRect.left) / sliderRect.width) * 100)
if (value !== props.value) {
props.onChange(value)
if (currentValue !== value) {
handleOnChange(currentValue)
}
}
function handleSliderClick(e: ChangeEvent<HTMLInputElement>) {
props.onChange(Number(e.target.value))
handleOnChange(Number(e.target.value))
}
function handleShowTooltip() {
@ -89,21 +106,22 @@ export default function Slider(props: Props) {
}
function getActiveIndex() {
if (props.value >= 100) return '5'
if (props.value >= 75) return '4'
if (props.value >= 50) return '3'
if (props.value >= 25) return '2'
if (value >= 100) return '5'
if (value >= 75) return '4'
if (value >= 50) return '3'
if (value >= 25) return '2'
return '1'
}
const DraggableElement: any = Draggable
const [positionOffset, position] = useMemo(() => {
debounceFunction()
return [
{ x: (props.value / 100) * -12, y: 0 },
{ x: (sliderRect.width / 100) * props.value, y: -2 },
{ x: (value / 100) * -12, y: 0 },
{ x: (sliderRect.width / 100) * value, y: -2 },
]
}, [props.value, sliderRect.width])
}, [value, sliderRect.width])
useEffect(() => {
handleSliderRect()
@ -115,60 +133,60 @@ export default function Slider(props: Props) {
ref={ref}
className={classNames(
'relative min-h-3 w-full transition-opacity',
props.className,
props.disabled && 'pointer-events-none',
className,
disabled && 'pointer-events-none',
)}
onMouseEnter={handleSliderRect}
>
<input
type='range'
value={props.value}
value={value}
onChange={handleSliderClick}
onMouseDown={handleShowTooltip}
className='absolute z-2 w-full hover:cursor-pointer appearance-none bg-transparent [&::-webkit-slider-thumb]:h-3 [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:appearance-none'
/>
<div className='absolute flex items-center w-full gap-1.5'>
<Mark
onClick={props.onChange}
onClick={() => handleOnChange(0)}
value={0}
sliderValue={props.value}
disabled={props.disabled}
sliderValue={value}
disabled={disabled}
style={{ backgroundColor: colors['1'] }}
/>
<Track maxValue={23} sliderValue={props.value} bg='before:gradient-slider-1' />
<Track maxValue={23} sliderValue={value} bg='before:gradient-slider-1' />
<Mark
onClick={props.onChange}
onClick={() => handleOnChange(25)}
value={25}
sliderValue={props.value}
disabled={props.disabled}
sliderValue={value}
disabled={disabled}
style={{ backgroundColor: colors['2'] }}
/>
<Track maxValue={48} sliderValue={props.value} bg='before:gradient-slider-2' />
<Track maxValue={48} sliderValue={value} bg='before:gradient-slider-2' />
<Mark
onClick={props.onChange}
onClick={() => handleOnChange(50)}
value={50}
sliderValue={props.value}
disabled={props.disabled}
sliderValue={value}
disabled={disabled}
style={{ backgroundColor: colors['3'] }}
/>
<Track maxValue={73} sliderValue={props.value} bg='before:gradient-slider-3' />
<Track maxValue={73} sliderValue={value} bg='before:gradient-slider-3' />
<Mark
onClick={props.onChange}
onClick={() => handleOnChange(75)}
value={75}
sliderValue={props.value}
disabled={props.disabled}
sliderValue={value}
disabled={disabled}
style={{ backgroundColor: colors['4'] }}
/>
<Track maxValue={98} sliderValue={props.value} bg='before:gradient-slider-4' />
<Track maxValue={98} sliderValue={value} bg='before:gradient-slider-4' />
<Mark
onClick={props.onChange}
onClick={() => handleOnChange(100)}
value={100}
sliderValue={props.value}
disabled={props.disabled}
sliderValue={value}
disabled={disabled}
style={{ backgroundColor: colors['5'] }}
/>
</div>
{!props.disabled && (
{!disabled && (
<div onMouseEnter={handleShowTooltip} onMouseLeave={handleHideTooltip}>
<DraggableElement
nodeRef={nodeRef}
@ -187,12 +205,12 @@ export default function Slider(props: Props) {
)}
style={{ background: colors[getActiveIndex()] }}
/>
{props.leverage ? (
{leverage ? (
<div className='pt-2.5'>
<LeverageLabel
leverage={props.leverage.current}
leverage={leverage.current}
decimals={1}
className={props.leverage.current >= 10 ? '-translate-x-2' : '-translate-x-1'}
className={leverage.current >= 10 ? '-translate-x-2' : '-translate-x-1'}
/>
</div>
) : (
@ -203,7 +221,7 @@ export default function Slider(props: Props) {
'absolute h-2 -translate-x-1/2 -bottom-2 left-1/2 -z-1 text-fuchsia',
)}
/>
{props.value.toFixed(0)}%
{value.toFixed(0)}%
</div>
)
)}
@ -212,19 +230,19 @@ export default function Slider(props: Props) {
</div>
)}
</div>
{props.leverage && (
{leverage && (
<div className='flex justify-between pt-2'>
<LeverageLabel
leverage={1}
decimals={0}
className='-translate-x-0.5'
style={{ opacity: props.value < 5 ? 0 : 1 }}
style={{ opacity: value < 5 ? 0 : 1 }}
/>
<LeverageLabel
leverage={props.leverage.max || 1}
leverage={leverage.max || 1}
decimals={0}
className='translate-x-1.5'
style={{ opacity: props.value > 95 ? 0 : 1 }}
style={{ opacity: value > 95 ? 0 : 1 }}
/>
</div>
)}

View File

@ -12,6 +12,7 @@ interface Props {
asset: Asset
max: BigNumber
onChange: (amount: BigNumber) => void
onDebounce?: () => void
accountId?: string
balances?: BNCoin[]
className?: string
@ -37,6 +38,11 @@ export default function TokenInputWithSlider(props: Props) {
props.onChange(newAmount)
}
function onDebounce() {
if (!props.onDebounce) return
props.onDebounce()
}
function onChangeAmount(newAmount: BigNumber) {
setAmount(newAmount)
setPercentage(BN(newAmount).dividedBy(props.max).multipliedBy(100).toNumber())
@ -76,6 +82,7 @@ export default function TokenInputWithSlider(props: Props) {
<Slider
value={percentage || 0}
onChange={(value) => onChangeSlider(value)}
onDebounce={onDebounce}
disabled={props.disabled}
leverage={props.leverage}
/>

View File

@ -61,6 +61,10 @@ export function PerpsModule() {
previousTradeDirection,
])
const onDebounce = useCallback(() => {
// TODO: Implement debounced simulation
}, [])
const setLeverage = useCallback((leverage: number) => {
// TODO: Implement leverage setting
}, [])
@ -116,6 +120,7 @@ export function PerpsModule() {
max={10}
value={leverage}
onChange={setLeverage}
onDebounce={onDebounce}
type={tradeDirection}
/>
<LeverageButtons />

View File

@ -201,7 +201,7 @@ export default function SwapForm(props: Props) {
isAutoLendEnabled && !isAutoRepayChecked ? 'lend' : 'deposit',
isAutoRepayChecked,
)
}, 100),
}, 250),
[simulateTrade, isAutoLendEnabled, isAutoRepayChecked],
)