From ead874a366b028aa7f55e28549ce013930f994d6 Mon Sep 17 00:00:00 2001 From: Thebora Kompanioni Date: Fri, 16 Aug 2024 15:15:08 +0200 Subject: [PATCH] refactor: quick freeze/unfreeze (#802) --- public/sprite.svg | 2 +- src/components/Balance.module.css | 36 +- src/components/Balance.tsx | 20 +- src/components/Divider.module.css | 32 ++ src/components/Divider.tsx | 27 + src/components/JarSelectorModal.tsx | 10 +- src/components/MainWalletView.module.css | 33 -- src/components/MainWalletView.tsx | 23 +- src/components/Modal.module.css | 10 +- src/components/Modal.tsx | 48 +- src/components/Send/AmountInputField.tsx | 21 +- src/components/Send/SendForm.tsx | 25 +- src/components/Send/ShowUtxos.module.css | 4 - src/components/Send/ShowUtxos.tsx | 506 ++++++++---------- src/components/Send/SourceJarSelector.tsx | 128 ++--- src/components/jar_details/UtxoList.tsx | 2 +- .../settings/FeeConfigModal.module.css | 1 - src/components/settings/FeeConfigModal.tsx | 2 +- src/context/WalletContext.tsx | 4 +- src/i18n/locales/en/translation.json | 5 +- 20 files changed, 449 insertions(+), 490 deletions(-) create mode 100644 src/components/Divider.module.css create mode 100644 src/components/Divider.tsx diff --git a/public/sprite.svg b/public/sprite.svg index c520136c1..61caf4245 100644 --- a/public/sprite.svg +++ b/public/sprite.svg @@ -364,7 +364,7 @@ - + diff --git a/src/components/Balance.module.css b/src/components/Balance.module.css index 013532ecd..d3051b06b 100644 --- a/src/components/Balance.module.css +++ b/src/components/Balance.module.css @@ -18,7 +18,7 @@ --jam-balance-deemphasize-color: #1153b5; } -.balance { +.balanceColor { color: var(--jam-balance-color); } @@ -53,32 +53,32 @@ justify-content: center; } -.bitcoinAmount .fractionalPart :nth-child(3)::before, -.bitcoinAmount .fractionalPart :nth-child(6)::before { +.bitcoinAmountSpacing .fractionalPart :nth-child(3)::before, +.bitcoinAmountSpacing .fractionalPart :nth-child(6)::before { content: '\202F'; } /** Integer Part **/ -.bitcoinAmount[data-integer-part-is-zero="true"] .integerPart, +.bitcoinAmountColor[data-integer-part-is-zero="true"] .integerPart, /** Decimal Point **/ -.bitcoinAmount[data-integer-part-is-zero="true"] .decimalPoint, -.bitcoinAmount[data-fractional-part-starts-with-zero="true"] .decimalPoint, +.bitcoinAmountColor[data-integer-part-is-zero="true"] .decimalPoint, +.bitcoinAmountColor[data-fractional-part-starts-with-zero="true"] .decimalPoint, /** Fractional Part **/ -.bitcoinAmount[data-integer-part-is-zero="false"] .fractionalPart, -.bitcoinAmount[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]), -.bitcoinAmount[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"], -.bitcoinAmount[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"] + span[data-digit="0"], -.bitcoinAmount[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"], -.bitcoinAmount[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"], -.bitcoinAmount[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"], -.bitcoinAmount[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"], -.bitcoinAmount[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"], +.bitcoinAmountColor[data-integer-part-is-zero="false"] .fractionalPart, +.bitcoinAmountColor[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]), +.bitcoinAmountColor[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"], +.bitcoinAmountColor[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"] + span[data-digit="0"], +.bitcoinAmountColor[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"], +.bitcoinAmountColor[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"], +.bitcoinAmountColor[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"], +.bitcoinAmountColor[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"], +.bitcoinAmountColor[data-integer-part-is-zero="true"] .fractionalPart :nth-child(1):is(span[data-digit="0"]) + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"] + span[data-digit="0"], /** Symbol */ -.bitcoinAmount[data-raw-value="0"] + .bitcoinSymbol { +.bitcoinAmountColor[data-raw-value="0"] + .bitcoinSymbol { color: var(--jam-balance-deemphasize-color); } -.satsAmount[data-raw-value='0'], -.satsAmount[data-raw-value='0'] + .satsSymbol { +.satsAmountColor[data-raw-value='0'], +.satsAmountColor[data-raw-value='0'] + .satsSymbol { color: var(--jam-balance-deemphasize-color); } diff --git a/src/components/Balance.tsx b/src/components/Balance.tsx index 645ee00a0..82ec691a8 100644 --- a/src/components/Balance.tsx +++ b/src/components/Balance.tsx @@ -39,7 +39,7 @@ interface BalanceComponentProps { symbol?: JSX.Element showSymbol?: boolean frozen?: boolean - isColorChange?: boolean + colored?: boolean frozenSymbol?: boolean } @@ -47,7 +47,7 @@ const BalanceComponent = ({ symbol, showSymbol = true, frozen = false, - isColorChange = false, + colored = true, frozenSymbol = true, children, }: PropsWithChildren) => { @@ -55,7 +55,7 @@ const BalanceComponent = ({ {children} @@ -69,7 +69,7 @@ const DECIMAL_POINT_CHAR = '.' type BitcoinBalanceProps = Omit & { value: number } -const BitcoinBalance = ({ value, ...props }: BitcoinBalanceProps) => { +const BitcoinBalance = ({ value, colored = true, ...props }: BitcoinBalanceProps) => { const numberString = formatBtc(value) const [integerPart, fractionalPart] = numberString.split(DECIMAL_POINT_CHAR) @@ -78,10 +78,10 @@ const BitcoinBalance = ({ value, ...props }: BitcoinBalanceProps) => { const fractionalPartStartsWithZero = fractionPartArray[0] === '0' return ( - + { type SatsBalanceProps = Omit & { value: number } -const SatsBalance = ({ value, ...props }: SatsBalanceProps) => { +const SatsBalance = ({ value, colored = true, ...props }: SatsBalanceProps) => { return ( - + diff --git a/src/components/Divider.module.css b/src/components/Divider.module.css new file mode 100644 index 000000000..814b9c424 --- /dev/null +++ b/src/components/Divider.module.css @@ -0,0 +1,32 @@ +.dividerContainer { + display: flex; + justify-content: space-between; + align-items: center; +} + +.dividerContainer .dividerLine { + margin: 0; + width: 50%; + flex-grow: 0; + flex-shrink: 1; +} + +.dividerContainer .dividerButton { + display: flex; + justify-content: center; + align-items: center; + margin: 0 1rem; + flex-shrink: 0; + flex-grow: 1; + color: var(--bs-body-color); + cursor: pointer; + background-color: transparent; + border: 1px solid var(--bs-body-color); + border-radius: 50%; + width: 2rem; + height: 2rem; +} + +.dividerContainer .dividerButton:disabled { + cursor: not-allowed; +} diff --git a/src/components/Divider.tsx b/src/components/Divider.tsx new file mode 100644 index 000000000..317f83040 --- /dev/null +++ b/src/components/Divider.tsx @@ -0,0 +1,27 @@ +import * as rb from 'react-bootstrap' +import classNames from 'classnames' +import Sprite from './Sprite' +import styles from './Divider.module.css' + +type DividerProps = rb.ColProps & { + toggled: boolean + onToggle: (current: boolean) => void + disabled?: boolean + className?: string +} + +export default function Divider({ toggled, onToggle, disabled, className, ...colProps }: DividerProps) { + return ( + + +
+
+ +
+
+
+
+ ) +} diff --git a/src/components/JarSelectorModal.tsx b/src/components/JarSelectorModal.tsx index 12045b339..0083db38e 100644 --- a/src/components/JarSelectorModal.tsx +++ b/src/components/JarSelectorModal.tsx @@ -91,9 +91,15 @@ export default function JarSelectorModal({ - {t('modal.confirm_button_reject')} + +
{t('modal.confirm_button_reject')}
- + {isConfirming ? ( <> diff --git a/src/components/MainWalletView.module.css b/src/components/MainWalletView.module.css index 893522a16..746af3fcd 100644 --- a/src/components/MainWalletView.module.css +++ b/src/components/MainWalletView.module.css @@ -27,36 +27,3 @@ width: 100%; height: 3.5rem; } - -.jarsDividerContainer { - display: flex; - justify-content: space-between; - align-items: center; -} - -.jarsDividerContainer .dividerLine { - margin: 0; - width: 50%; - flex-grow: 0; - flex-shrink: 1; -} - -.jarsDividerContainer .dividerButton { - display: flex; - justify-content: center; - align-items: center; - margin: 0 1rem; - flex-shrink: 0; - flex-grow: 1; - color: var(--bs-body-color); - cursor: pointer; - background-color: transparent; - border: 1px solid var(--bs-body-color); - border-radius: 50%; - width: 2rem; - height: 2rem; -} - -.jarsDividerContainer .dividerButton:disabled { - cursor: not-allowed; -} diff --git a/src/components/MainWalletView.tsx b/src/components/MainWalletView.tsx index efadf1d18..dc74df223 100644 --- a/src/components/MainWalletView.tsx +++ b/src/components/MainWalletView.tsx @@ -13,6 +13,7 @@ import { ExtendedLink } from './ExtendedLink' import { JarDetailsOverlay } from './jar_details/JarDetailsOverlay' import { Jars } from './Jars' import styles from './MainWalletView.module.css' +import Divider from './Divider' interface WalletHeaderProps { walletName: string @@ -226,21 +227,13 @@ export default function MainWalletView({ wallet }: MainWalletViewProps) { - - -
-
- -
-
-
-
+ setShowJars((current) => !current)} + disabled={serviceInfo?.rescanning} + xs={showJars ? 12 : 10} + md={showJars ? 12 : 8} + /> ) diff --git a/src/components/Modal.module.css b/src/components/Modal.module.css index 40ca36445..6c2dee561 100644 --- a/src/components/Modal.module.css +++ b/src/components/Modal.module.css @@ -8,7 +8,7 @@ box-shadow: 0px 0px 24px rgba(0, 0, 0, 0.25) !important; } -.modal-header { +.modalHeader { display: flex !important; justify-content: center !important; background-color: transparent !important; @@ -16,20 +16,20 @@ padding: 1.25rem 1.25rem 0 1.25rem !important; } -.modal-title { +.modalTitle { font-size: 1.3rem !important; font-weight: 600 !important; color: var(--bs-body-color) !important; } -.modal-body { +.modalBody { text-align: center !important; font-size: 1rem !important; font-weight: 400 !important; padding: 0.25rem 1.25rem 1rem 1.25rem !important; } -.modal-footer { +.modalFooter { display: flex !important; justify-content: center !important; gap: 1rem; @@ -37,7 +37,7 @@ padding: 1rem 1.25rem 1.25rem 1.25rem !important; } -.modal-footer :global .btn { +.modalFooter :global .btn { --bs-btn-border-color: var(--bs-border-color); flex-grow: 1; min-height: 2.8rem; diff --git a/src/components/Modal.tsx b/src/components/Modal.tsx index 40e7c4b36..0a1c60624 100644 --- a/src/components/Modal.tsx +++ b/src/components/Modal.tsx @@ -1,19 +1,18 @@ import { ReactNode, PropsWithChildren } from 'react' import * as rb from 'react-bootstrap' import { useTranslation } from 'react-i18next' -import styles from './Modal.module.css' import Sprite from './Sprite' +import styles from './Modal.module.css' + +type BaseModalProps = Pick & + Pick & { + isShown: boolean + title: ReactNode | string + onCancel: () => void + headerClassName?: rb.ModalHeaderProps['className'] + titleClassName?: rb.ModalTitleProps['className'] + } -type BaseModalProps = { - isShown: boolean - title: ReactNode | string - onCancel: () => void - backdrop?: rb.ModalProps['backdrop'] - size?: rb.ModalProps['size'] - showCloseButton?: boolean - headerClassName?: string - titleClassName?: string -} const BaseModal = ({ isShown, title, @@ -21,9 +20,10 @@ const BaseModal = ({ onCancel, size, backdrop = 'static', - showCloseButton = false, - headerClassName, - titleClassName, + closeButton = false, + className = styles.modal, + headerClassName = styles.modalHeader, + titleClassName = styles.modalTitle, }: PropsWithChildren) => { return ( - - {title} + + {title} {children} @@ -59,8 +59,8 @@ const InfoModal = ({ }: PropsWithChildren) => { return ( - {children} - + {children} + onSubmit()}> {submitButtonText} @@ -72,7 +72,6 @@ const InfoModal = ({ export type ConfirmModalProps = BaseModalProps & { onConfirm: () => void disabled?: boolean - confirmVariant?: string } const ConfirmModal = ({ @@ -80,15 +79,14 @@ const ConfirmModal = ({ onCancel, onConfirm, disabled = false, - confirmVariant = 'outline-dark', ...baseModalProps }: PropsWithChildren) => { const { t } = useTranslation() return ( - {children} - + {children} + onCancel()} @@ -97,7 +95,7 @@ const ConfirmModal = ({
{t('modal.confirm_button_reject')}
- onConfirm()} disabled={disabled}> + onConfirm()} disabled={disabled}> {t('modal.confirm_button_accept')}
@@ -105,4 +103,4 @@ const ConfirmModal = ({ ) } -export { InfoModal, ConfirmModal } +export { BaseModal, InfoModal, ConfirmModal } diff --git a/src/components/Send/AmountInputField.tsx b/src/components/Send/AmountInputField.tsx index 522f53812..e16a89340 100644 --- a/src/components/Send/AmountInputField.tsx +++ b/src/components/Send/AmountInputField.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react' +import { useRef } from 'react' import { useTranslation } from 'react-i18next' import * as rb from 'react-bootstrap' import { useField, useFormikContext } from 'formik' @@ -35,25 +35,6 @@ export const AmountInputField = ({ const form = useFormikContext() const ref = useRef(null) - //Effect to change the field value whenever the sourceJarBalance changes (sourceJarBalance will change when quick freeze/unfreeze is performed or different source jar is selected) - useEffect(() => { - if (!sourceJarBalance) return - - const currentValue = formatBtcDisplayValue(sourceJarBalance.calculatedAvailableBalanceInSats) - - if (field.value?.isSweep && field.value.displayValue !== currentValue) { - form.setFieldValue( - field.name, - { - value: 0, - isSweep: true, - displayValue: formatBtcDisplayValue(sourceJarBalance.calculatedAvailableBalanceInSats), - }, - true, - ) - } - }, [sourceJarBalance, field, form]) - return ( <> diff --git a/src/components/Send/SendForm.tsx b/src/components/Send/SendForm.tsx index 7eeaf130b..5d051b7ea 100644 --- a/src/components/Send/SendForm.tsx +++ b/src/components/Send/SendForm.tsx @@ -1,6 +1,6 @@ -import { useState, useMemo } from 'react' +import { useState, useMemo, useEffect } from 'react' import { Trans, useTranslation } from 'react-i18next' -import { Field, Formik, FormikErrors, FormikProps } from 'formik' +import { Field, Formik, FormikErrors, FormikProps, useField } from 'formik' import * as rb from 'react-bootstrap' import * as Api from '../../libs/JmWalletApi' import ToggleSwitch from '../ToggleSwitch' @@ -243,6 +243,9 @@ const InnerSendForm = ({ }: InnerSendFormProps) => { const { t } = useTranslation() const serviceInfo = useServiceInfo() + const amountField = useField('amount') + const amountMeta = amountField[1] + const amountHelper = amountField[2] const jarBalances = useMemo(() => { if (!walletInfo) return [] @@ -261,12 +264,26 @@ const InnerSendForm = ({ return buildCoinjoinRequirementSummary(sourceJarUtxos) }, [sourceJarUtxos]) - const sourceJarBalance = - props.values.sourceJarIndex !== undefined ? jarBalances[props.values.sourceJarIndex] : undefined + const sourceJarBalance = useMemo( + () => (props.values.sourceJarIndex !== undefined ? jarBalances[props.values.sourceJarIndex] : undefined), + [jarBalances, props.values.sourceJarIndex], + ) const showCoinjoinPreconditionViolationAlert = !isLoading && !disabled && props.values.isCoinJoin && sourceJarCoinjoinPreconditionSummary?.isFulfilled === false + //Effect to change the field value whenever the sourceJarBalance changes (sourceJarBalance will change when quick freeze/unfreeze is performed or different source jar is selected) + useEffect(() => { + if (!sourceJarBalance) return + amountHelper.setValue( + amountMeta.initialValue || { + value: null, + isSweep: false, + }, + true, + ) + }, [sourceJarBalance, amountHelper, amountMeta.initialValue]) + return ( <> diff --git a/src/components/Send/ShowUtxos.module.css b/src/components/Send/ShowUtxos.module.css index 38998a9df..97d512a17 100644 --- a/src/components/Send/ShowUtxos.module.css +++ b/src/components/Send/ShowUtxos.module.css @@ -18,10 +18,6 @@ color: #eb5757 !important; } -.subTitle { - color: #777777 !important; -} - .utxoTagDeposit { color: #999999; border: 1px solid #bbbbbb; diff --git a/src/components/Send/ShowUtxos.tsx b/src/components/Send/ShowUtxos.tsx index 03eacd75b..8714de224 100755 --- a/src/components/Send/ShowUtxos.tsx +++ b/src/components/Send/ShowUtxos.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback, memo, useMemo } from 'react' +import { useState, useMemo } from 'react' import * as rb from 'react-bootstrap' import { useTranslation } from 'react-i18next' import type { TFunction } from 'i18next' @@ -6,80 +6,50 @@ import classNames from 'classnames' import { Table, Body, Row, Cell } from '@table-library/react-table-library/table' import { useTheme } from '@table-library/react-table-library/theme' import * as TableTypes from '@table-library/react-table-library/types/table' -import { WalletInfo, Utxo, useCurrentWalletInfo } from '../../context/WalletContext' +import { WalletInfo, Utxo, useCurrentWalletInfo, Utxos } from '../../context/WalletContext' import { useSettings, Settings } from '../../context/SettingsContext' import Alert from '../Alert' import Balance from '../Balance' -import { ConfirmModal } from '../Modal' +import Divider from '../Divider' +import { BaseModal } from '../Modal' import Sprite from '../Sprite' import { utxoTags } from '../jar_details/UtxoList' -import mainStyles from '../MainWalletView.module.css' +import { shortenStringMiddle } from '../../utils' import styles from './ShowUtxos.module.css' -import { UtxoList } from './SourceJarSelector' interface ShowUtxosProps { isOpen: boolean - onCancel: () => void - onConfirm: () => void - alert: SimpleAlert | undefined isLoading: boolean - frozenUtxos: UtxoList - unFrozenUtxos: UtxoList - setFrozenUtxos: (arg: UtxoList) => void - setUnFrozenUtxos: (arg: UtxoList) => void + utxos: Utxos + alert?: SimpleAlert + onCancel: () => void + onConfirm: (selectedUtxos: Utxos) => void } interface UtxoRowProps { - utxo: Utxo - utxoIndex: number - onToggle?: (index: number, isFrozen: boolean) => void - isFrozen: boolean + utxo: SelectableUtxoTableRowData + onToggle: (utxo: SelectableUtxoTableRowData) => void settings: Settings - showRadioButton: boolean showBackgroundColor: boolean walletInfo: WalletInfo t: TFunction } interface UtxoListDisplayProps { - utxos: Array - onToggle?: (index: number, isFrozen: boolean) => void + utxos: SelectableUtxo[] + onToggle: (utxo: SelectableUtxo) => void settings: Settings - showRadioButton: boolean showBackgroundColor: boolean } -interface DividerProps { - isState: boolean - setIsState: (arg: boolean) => void - className?: string -} - -// Utility function to format Bitcoin address -const formatAddress = (address: string) => `${address.slice(0, 10)}...${address.slice(-8)}` - -// Utility function to format the confirmations -const formatConfirmations = (conf: number) => { - if (conf === 0) return { symbol: 'confs-0', confirmations: conf } - if (conf === 1) return { symbol: 'confs-1', confirmations: conf } - if (conf === 2) return { symbol: 'confs-2', confirmations: conf } - if (conf === 3) return { symbol: 'confs-3', confirmations: conf } - if (conf === 4) return { symbol: 'confs-4', confirmations: conf } - if (conf === 5) return { symbol: 'confs-5', confirmations: conf } - if (conf > 9999) return { symbol: 'confs-full', confirmations: '9999+' } - return { symbol: 'confs-full', confirmations: conf } -} - -// Utility function to convert Satoshi to Bitcoin -const satsToBtc = (sats: number) => (sats / 100000000).toFixed(8) - // Utility function to Identifies Icons const utxoIcon = (tag: string, isFrozen: boolean) => { if (isFrozen && tag === 'bond') return 'timelock' if (isFrozen) return 'snowflake' - if (tag === 'deposit' || tag === 'non-cj-change' || tag === 'reused') return 'Unmixed' if (tag === 'bond') return 'timelock' - return 'mixed' + if (tag === 'cj-out') return 'mixed' + if (tag === 'deposit' || tag === 'non-cj-change' || tag === 'reused') return 'unmixed' + return 'unmixed' // fallback } // Utility function to allot classes @@ -92,156 +62,153 @@ const allotClasses = (tag: string, isFrozen: boolean) => { return { row: styles.depositUtxo, tag: styles.utxoTagDeposit } } -const UtxoRow = memo( - ({ - utxo, - utxoIndex, - onToggle, - isFrozen, - showRadioButton, - showBackgroundColor, - settings, - walletInfo, - t, - }: UtxoRowProps) => { - const { address: utxoAddress, confirmations, value, checked, frozen } = utxo +interface ConfirmationFormat { + symbol: string + display: string + confirmations: number +} - const address = useMemo(() => formatAddress(utxoAddress), [utxoAddress]) - const conf = useMemo(() => formatConfirmations(confirmations), [confirmations]) - const valueString = useMemo(() => satsToBtc(value).toString(), [value]) - const tag = useMemo(() => utxoTags(utxo, walletInfo, t), [utxo, walletInfo, t]) +const formatConfirmations = (confirmations: number): ConfirmationFormat => ({ + symbol: `confs-${confirmations >= 6 ? 'full' : confirmations}`, + display: confirmations > 9999 ? `${Number(9999).toLocaleString()}+` : confirmations.toLocaleString(), + confirmations, +}) - const { icon, rowAndTagClass } = useMemo(() => { - if (tag.length === 0) { - return { icon: 'Unmixed', rowAndTagClass: { row: styles.depositUtxo, tag: styles.utxoTagDeposit } } - } - return { icon: utxoIcon(tag[0].tag, isFrozen), rowAndTagClass: allotClasses(tag[0].tag, isFrozen) } - }, [tag, isFrozen]) +const Confirmations = ({ value }: { value: ConfirmationFormat }) => + value.confirmations > 9999 ? ( + {value.confirmations.toLocaleString()}} + > +
+ + {value.display} +
+
+ ) : ( +
+ + {value.display} +
+ ) - const ConfirmationCell = () => - confirmations > 9999 ? ( - {confirmations}} - > -
- - {conf.confirmations} -
-
- ) : ( -
- - {conf.confirmations} -
- ) +const UtxoRow = ({ utxo, onToggle, showBackgroundColor, settings, walletInfo, t }: UtxoRowProps) => { + const address = useMemo(() => shortenStringMiddle(utxo.address, 16), [utxo.address]) + const confFormat = useMemo(() => formatConfirmations(utxo.confirmations), [utxo.confirmations]) + const tag = useMemo(() => utxoTags(utxo, walletInfo, t), [utxo, walletInfo, t]) - return ( - onToggle && onToggle(utxoIndex, frozen)} - > - {showRadioButton && ( - - { - onToggle && onToggle(utxoIndex, isFrozen) - }} - className={classNames(isFrozen ? styles.squareFrozenToggleButton : styles.squareToggleButton, { - [styles.selected]: checked, - })} - /> - - )} - - - - {address} - - - - - { + if (tag.length === 0) { + return { icon: 'unmixed', rowAndTagClass: { row: styles.depositUtxo, tag: styles.utxoTagDeposit } } + } + return { icon: utxoIcon(tag[0].tag, utxo.frozen), rowAndTagClass: allotClasses(tag[0].tag, utxo.frozen) } + }, [tag, utxo.frozen]) + + return ( + utxo.selectable && onToggle(utxo)} + > + + {utxo.selectable && ( + utxo.selectable && onToggle(utxo)} + className={classNames(utxo.frozen ? styles.squareFrozenToggleButton : styles.squareToggleButton, { + [styles.selected]: utxo.checked, + })} /> - - -
{tag.length ? tag[0].tag : ''}
-
-
- ) - }, -) + )} +
+ + + + {address} + + + + + + + +
{tag.length ? tag[0].tag : ''}
+
+
+ ) +} + +type SelectableUtxoTableRowData = SelectableUtxo & { + // TODO: add "tags" here and remove from "Utxo" type + // tags?: { tag: string; color: string }[] +} & Pick -const UtxoListDisplay = ({ - utxos, - onToggle, - settings, - showRadioButton = true, - showBackgroundColor = true, -}: UtxoListDisplayProps) => { +const UtxoListDisplay = ({ utxos, onToggle, settings, showBackgroundColor = true }: UtxoListDisplayProps) => { const { t } = useTranslation() const walletInfo = useCurrentWalletInfo() - //Table theme to manage view const TABLE_THEME = { Table: ` - font-size: ${showRadioButton ? '1rem' : '0.87rem'}; - --data-table-library_grid-template-columns: ${showRadioButton ? '3.5rem 2.5rem 12rem 2fr 3fr 10rem ' : '2.5rem 10rem 5fr 3fr 7.5rem'}; + --data-table-library_grid-template-columns: 3.5rem 2.5rem 12rem 2fr 3fr 10rem; @media only screen and (min-width: 768px) { - --data-table-library_grid-template-columns: ${showRadioButton ? '3.5rem 2.5rem 14rem 5fr 3fr 10rem' : '2.5rem 11rem 5fr 3fr 7.5rem'}; + --data-table-library_grid-template-columns: 3.5rem 2.5rem 14rem 5fr 3fr 10rem}; } `, BaseCell: ` - padding:${showRadioButton ? '0.5rem' : '0.55rem'} 0.35rem !important; + padding: 0.35rem 0.25rem !important; margin: 0.15rem 0px !important; `, } const tableTheme = useTheme(TABLE_THEME) - //Default sort is by date the older ones at the bottom, newer ones at the top. - utxos.sort((a, b) => a.confirmations - b.confirmations) + const tableData: TableTypes.Data = useMemo( + () => ({ + nodes: utxos.map( + (utxo: Utxo) => + ({ + ...utxo, + id: utxo.utxo, + }) as SelectableUtxoTableRowData, + ), + }), + [utxos], + ) return (
- - {(utxosList: TableTypes.TableProps) => ( +
+ {(utxosList: TableTypes.TableProps) => ( {walletInfo && - utxosList.map((utxo: Utxo, index: number) => { + utxosList.map((utxo: SelectableUtxoTableRowData, index: number) => { return ( { - //Effect for getting back to it's original state when components unMounts - useEffect(() => { - return () => { - setIsState(false) - } - }, [setIsState]) - - return ( - - -
-
- -
-
-
-
- ) -} +type SelectableUtxo = Utxo & { checked: boolean; selectable: boolean } -const ShowUtxos = ({ - isOpen, - onCancel, - onConfirm, - alert, - isLoading, - frozenUtxos, - unFrozenUtxos, - setFrozenUtxos, - setUnFrozenUtxos, -}: ShowUtxosProps) => { +// TODO: rename to QuickFreezeUtxosModal? +const ShowUtxos = ({ isOpen, onCancel, onConfirm, isLoading, utxos, alert }: ShowUtxosProps) => { const { t } = useTranslation() const settings = useSettings() - const [showFrozenUtxos, setShowFrozenUtxos] = useState(false) - // Handler to toggle UTXO selection - const handleUtxoCheckedState = useCallback( - (utxoIndex: number, isFrozen: boolean) => { - if (!isFrozen) { - const utxos = unFrozenUtxos.map((utxo: Utxo, i: number) => - i === utxoIndex ? { ...utxo, checked: !utxo.checked } : utxo, - ) - setUnFrozenUtxos(utxos) - } else { - const utxos = frozenUtxos.map((utxo: Utxo, i: number) => - i === utxoIndex ? { ...utxo, checked: !utxo.checked } : utxo, - ) - setFrozenUtxos(utxos) - } - }, - [frozenUtxos, unFrozenUtxos, setUnFrozenUtxos, setFrozenUtxos], + const [upperUtxos, setUpperUtxos] = useState( + utxos + .filter((it) => !it.frozen) + .filter((it) => !it.locktime) + .map((it) => ({ ...it, checked: true, selectable: true })) + .sort((a, b) => a.confirmations - b.confirmations), ) + const [lowerUtxos, setLowerUtxos] = useState(() => { + const frozenNonTimelockedUtxos = utxos + .filter((it) => it.frozen) + .filter((it) => !it.locktime) + .map((it) => ({ ...it, checked: false, selectable: true })) + .sort((a, b) => a.confirmations - b.confirmations) - //Effect to hide the Divider line when there is no unFrozen-UTXOs present - useEffect(() => { - if (unFrozenUtxos.length === 0 && frozenUtxos.length > 0) { - setShowFrozenUtxos(true) - } - }, [unFrozenUtxos.length, frozenUtxos.length]) + const timelockedUtxos = utxos + .filter((it) => it.locktime !== undefined) + .map((it) => ({ ...it, checked: false, selectable: false })) + .sort((a, b) => a.confirmations - b.confirmations) + + return [...frozenNonTimelockedUtxos, ...timelockedUtxos] + }) + + const selectedUtxos = useMemo( + () => [...upperUtxos, ...lowerUtxos].filter((it) => it.checked), + [upperUtxos, lowerUtxos], + ) + const [showFrozenUtxos, setShowFrozenUtxos] = useState(upperUtxos.length === 0 && lowerUtxos.length > 0) return ( - - {!isLoading ? ( - <> -
- {unFrozenUtxos.length !== 0 - ? t('show_utxos.show_utxo_subtitle') - : t('show_utxos.show_utxo_subtitle_when_allutxos_are_frozen')} + + {isLoading ? ( +
+
- {alert && ( + ) : ( + <> + + {upperUtxos.length > 0 + ? t('show_utxos.show_utxo_subtitle') + : t('show_utxos.show_utxo_subtitle_when_allutxos_are_frozen')} + + {alert && ( + + + + )} - + { + setUpperUtxos((current) => + current.map((it) => (it.utxo !== utxo.utxo ? it : { ...it, checked: !utxo.checked })), + ) + }} + settings={settings} + showBackgroundColor={true} + /> - )} - - {frozenUtxos.length > 0 && unFrozenUtxos.length > 0 && ( - - )} - {showFrozenUtxos && ( - - )} - - ) : ( -
-
- )} - + {upperUtxos.length > 0 && lowerUtxos.length > 0 && ( + setShowFrozenUtxos((current) => !current)} + className={`mt-4 ${showFrozenUtxos && 'mb-4'}`} + /> + )} + + + { + setLowerUtxos((current) => + current.map((it) => (it.utxo !== utxo.utxo ? it : { ...it, checked: !utxo.checked })), + ) + }} + settings={settings} + showBackgroundColor={true} + /> + + + + )} +
+ + onCancel()} + className="d-flex flex-1 justify-content-center align-items-center" + > + +
{t('modal.confirm_button_reject')}
+
+ onConfirm(selectedUtxos)} + disabled={isLoading} + className="d-flex flex-1 justify-content-center align-items-center" + > + {t('modal.confirm_button_accept')} + +
+ ) } -export { ShowUtxos, Divider, UtxoListDisplay, UtxoRow } +export { ShowUtxos } diff --git a/src/components/Send/SourceJarSelector.tsx b/src/components/Send/SourceJarSelector.tsx index af6e9ccb5..a6e8e516b 100644 --- a/src/components/Send/SourceJarSelector.tsx +++ b/src/components/Send/SourceJarSelector.tsx @@ -1,9 +1,9 @@ -import { useState, useMemo, useCallback, useEffect } from 'react' +import { useState, useMemo, useCallback } from 'react' import { useField, useFormikContext } from 'formik' import * as rb from 'react-bootstrap' import { jarFillLevel, SelectableJar } from '../jars/Jar' import { noop } from '../../utils' -import { WalletInfo, CurrentWallet, useReloadCurrentWalletInfo, Utxo } from '../../context/WalletContext' +import { WalletInfo, CurrentWallet, useReloadCurrentWalletInfo, Utxos } from '../../context/WalletContext' import styles from './SourceJarSelector.module.css' import { ShowUtxos } from './ShowUtxos' import { useTranslation } from 'react-i18next' @@ -21,12 +21,11 @@ export type SourceJarSelectorProps = { } interface ShowUtxosProps { - jarIndex?: string - isOpen?: boolean + utxos: Utxos + isLoading: boolean + alert?: SimpleAlert } -export type UtxoList = Utxo[] - export const SourceJarSelector = ({ name, label, @@ -41,11 +40,9 @@ export const SourceJarSelector = ({ const form = useFormikContext() const reloadCurrentWalletInfo = useReloadCurrentWalletInfo() - const [showUtxos, setShowUtxos] = useState(undefined) - const [alert, setAlert] = useState(undefined) - const [isUtxosLoading, setIsUtxosLoading] = useState(false) - const [unFrozenUtxos, setUnFrozenUtxos] = useState([]) - const [frozenUtxos, setFrozenUtxos] = useState([]) + const [showUtxos, setShowUtxos] = useState() + //const [unFrozenUtxos, setUnFrozenUtxos] = useState([]) + //const [frozenUtxos, setFrozenUtxos] = useState([]) const jarBalances = useMemo(() => { if (!walletInfo) return [] @@ -54,24 +51,7 @@ export const SourceJarSelector = ({ ) }, [walletInfo]) - useEffect(() => { - if (showUtxos?.jarIndex && walletInfo?.utxosByJar) { - const data = Object.entries(walletInfo.utxosByJar).find(([key]) => key === showUtxos.jarIndex) - const utxos: any = data ? data[1] : [] - - const frozenUtxoList = utxos - .filter((utxo: any) => utxo.frozen) - .map((utxo: any) => ({ ...utxo, id: utxo.utxo, checked: false })) - const unFrozenUtxosList = utxos - .filter((utxo: any) => !utxo.frozen) - .map((utxo: any) => ({ ...utxo, id: utxo.utxo, checked: true })) - - setFrozenUtxos(frozenUtxoList) - setUnFrozenUtxos(unFrozenUtxosList) - } - }, [walletInfo, showUtxos?.jarIndex, t]) - - useEffect(() => { + /*useEffect(() => { if (frozenUtxos.length === 0 && unFrozenUtxos.length === 0) { return } @@ -90,37 +70,44 @@ export const SourceJarSelector = ({ } else { setAlert(undefined) } - }, [frozenUtxos, unFrozenUtxos, t, setAlert]) - - const handleUtxosFrozenState = useCallback(async () => { - const abortCtrl = new AbortController() - const frozenUtxosToUpdate = frozenUtxos - .filter((utxo) => utxo.checked && !utxo.locktime) - .map((utxo) => ({ utxo: utxo.utxo, freeze: false })) - const unFrozenUtxosToUpdate = unFrozenUtxos - .filter((utxo) => !utxo.checked) - .map((utxo) => ({ utxo: utxo.utxo, freeze: true })) - - try { - const res = await Promise.all([ - ...frozenUtxosToUpdate.map((utxo) => Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, utxo)), - ...unFrozenUtxosToUpdate.map((utxo) => Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, utxo)), - ]) - - if (res.length !== 0) { - setIsUtxosLoading(true) - await reloadCurrentWalletInfo.reloadUtxos({ signal: abortCtrl.signal }) - } - - setShowUtxos(undefined) - } catch (err: any) { - if (!abortCtrl.signal.aborted) { - setAlert({ variant: 'danger', message: err.message, dismissible: true }) + }, [frozenUtxos, unFrozenUtxos, t, setAlert])*/ + + const handleUtxosFrozenState = useCallback( + async (selectedUtxos: Utxos) => { + if (!showUtxos) return + + const abortCtrl = new AbortController() + + const selectedUtxosIds = selectedUtxos.map((it) => it.utxo) + const frozenUtxosToUnfreeze = selectedUtxos.filter((utxo) => utxo.frozen) + const unfrozenUtxosToFreeze = showUtxos.utxos + .filter((utxo) => !utxo.frozen) + .filter((it) => !selectedUtxosIds.includes(it.utxo)) + + try { + setShowUtxos({ ...showUtxos, isLoading: true, alert: undefined }) + + const res = await Promise.all([ + ...frozenUtxosToUnfreeze.map((utxo) => + Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, { utxo: utxo.utxo, freeze: false }), + ), + ...unfrozenUtxosToFreeze.map((utxo) => + Api.postFreeze({ ...wallet, signal: abortCtrl.signal }, { utxo: utxo.utxo, freeze: true }), + ), + ]) + + if (res.length !== 0) { + await reloadCurrentWalletInfo.reloadUtxos({ signal: abortCtrl.signal }) + } + + setShowUtxos(undefined) + } catch (err: any) { + if (abortCtrl.signal.aborted) return + setShowUtxos({ ...showUtxos, isLoading: false, alert: { variant: 'danger', message: err.message } }) } - } finally { - setIsUtxosLoading(false) - } - }, [frozenUtxos, unFrozenUtxos, wallet, reloadCurrentWalletInfo]) + }, + [showUtxos, wallet, reloadCurrentWalletInfo], + ) return ( <> @@ -132,19 +119,14 @@ export const SourceJarSelector = ({ ) : (
- {showUtxos?.isOpen && ( + {showUtxos && ( { - setShowUtxos(undefined) - }} - alert={alert} - isLoading={isUtxosLoading} - frozenUtxos={frozenUtxos} - unFrozenUtxos={unFrozenUtxos} - setFrozenUtxos={setFrozenUtxos} - setUnFrozenUtxos={setUnFrozenUtxos} + onCancel={() => setShowUtxos(undefined)} /> )} {jarBalances.map((it) => { @@ -163,7 +145,7 @@ export const SourceJarSelector = ({ walletInfo.balanceSummary.calculatedTotalBalanceInSats, )} variant={it.accountIndex === field.value ? variant : undefined} - onClick={(jarIndex: number) => { + onClick={(jarIndex) => { form.setFieldValue(field.name, jarIndex, true) if ( it.accountIndex === field.value && @@ -172,8 +154,8 @@ export const SourceJarSelector = ({ it.calculatedTotalBalanceInSats > 0 ) { setShowUtxos({ - jarIndex: it.accountIndex.toString(), - isOpen: true, + utxos: walletInfo.utxosByJar[it.accountIndex], + isLoading: false, }) } }} diff --git a/src/components/jar_details/UtxoList.tsx b/src/components/jar_details/UtxoList.tsx index f6be3702b..e782f0800 100644 --- a/src/components/jar_details/UtxoList.tsx +++ b/src/components/jar_details/UtxoList.tsx @@ -156,7 +156,7 @@ const toUtxo = (tableNode: TableTypes.TableNode): Utxo => { return utxo as Utxo } -interface UtxoTableRow extends Utxo { +interface UtxoTableRow extends Utxo, TableTypes.TableNode { _icon: JSX.Element _tags: Tag[] _confs: JSX.Element diff --git a/src/components/settings/FeeConfigModal.module.css b/src/components/settings/FeeConfigModal.module.css index faf340e8b..b16cbd893 100644 --- a/src/components/settings/FeeConfigModal.module.css +++ b/src/components/settings/FeeConfigModal.module.css @@ -33,7 +33,6 @@ .modalFooter .buttonContainer :global .btn { flex-grow: 1; - min-height: 2.8rem; font-weight: 500; border-color: none !important; } diff --git a/src/components/settings/FeeConfigModal.tsx b/src/components/settings/FeeConfigModal.tsx index 581be1bcd..d38604774 100644 --- a/src/components/settings/FeeConfigModal.tsx +++ b/src/components/settings/FeeConfigModal.tsx @@ -10,9 +10,9 @@ import { useUpdateConfigValues } from '../../context/ServiceConfigContext' import { isDebugFeatureEnabled } from '../../constants/debugFeatures' import ToggleSwitch from '../ToggleSwitch' import { isValidNumber, factorToPercentage, percentageToFactor } from '../../utils' -import styles from './FeeConfigModal.module.css' import BitcoinAmountInput, { AmountValue, toAmountValue } from '../BitcoinAmountInput' import { JM_MAX_SWEEP_FEE_CHANGE_DEFAULT } from '../../constants/config' +import styles from './FeeConfigModal.module.css' const __dev_allowFeeValuesReset = isDebugFeatureEnabled('allowFeeValuesReset') diff --git a/src/context/WalletContext.tsx b/src/context/WalletContext.tsx index 5632f7975..080df1ae4 100644 --- a/src/context/WalletContext.tsx +++ b/src/context/WalletContext.tsx @@ -53,8 +53,7 @@ export type Utxo = { // `locktime` in format "yyyy-MM-dd 00:00:00" // NOTE: it is unparsable with safari Date constructor locktime?: string - id: string - checked?: boolean + // TODO: remove 'tags' prop tags?: { tag: string; color: string }[] } @@ -195,7 +194,6 @@ export const groupByJar = (utxos: Utxos): UtxosByJar => { return utxos.reduce((res, utxo) => { const { mixdepth } = utxo res[mixdepth] = res[mixdepth] || [] - utxo.id = utxo.utxo res[mixdepth].push(utxo) return res }, {} as UtxosByJar) diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index fcbdf99f4..00a3de510 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -706,9 +706,6 @@ "selected_utxos": "Selected UTXOs", "show_utxo_title": "Select UTXOs to be considered", "show_utxo_subtitle": "The following UTXOs are considered in the transaction. Every unselected UTXO will be frozen and can be unfrozen later on.", - "show_utxo_subtitle_when_allutxos_are_frozen": "The following UTXOs are frozen. Please select them to be considered in the transaction.", - "alert_for_unfreeze_utxos": "At least one UTXO is required to perform a transaction", - "alert_for_time_locked": "Selected UTXO is Time Locked till", - "alert_for_empty_utxos": "Please Unfreeze UTXOs to send" + "show_utxo_subtitle_when_allutxos_are_frozen": "The following UTXOs are frozen. Please select them to be considered in the transaction." } }