ui(Send): hide "number of collaborators" behind send options (#595)

This commit is contained in:
Nicola Elia 2023-01-17 16:39:48 +01:00 committed by GitHub
parent da27812406
commit 9d1575ffec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 213 additions and 237 deletions

View file

@ -0,0 +1,33 @@
import { PropsWithChildren, useState } from 'react'
import { useSettings } from '../context/SettingsContext'
import * as rb from 'react-bootstrap'
import Sprite from './Sprite'
interface AccordionProps {
title: string
defaultOpen?: boolean
}
const Accordion = ({ title, defaultOpen = false, children }: PropsWithChildren<AccordionProps>) => {
const settings = useSettings()
const [isOpen, setIsOpen] = useState(defaultOpen)
return (
<div className="mt-4">
<rb.Button
variant={settings.theme}
className="d-flex align-items-center bg-transparent border-0 w-100 px-0 py-2"
onClick={() => setIsOpen((current) => !current)}
>
{title}
<Sprite symbol={`caret-${isOpen ? 'up' : 'down'}`} className="ms-1" width="20" height="20" />
</rb.Button>
<hr className="m-0 pb-4 text-secondary" />
<rb.Collapse in={isOpen}>
<div>{children}</div>
</rb.Collapse>
</div>
)
}
export default Accordion

View file

@ -18,6 +18,7 @@ import { EarnReportOverlay } from './EarnReport'
import { OrderbookOverlay } from './Orderbook'
import Balance from './Balance'
import styles from './Earn.module.css'
import Accordion from './Accordion'
// In order to prevent state mismatch, the 'maker stop' response is delayed shortly.
// Even though the API response suggests that the maker has started or stopped immediately, it seems that this is not always the case.
@ -176,7 +177,6 @@ export default function Earn({ wallet }) {
const serviceInfo = useServiceInfo()
const reloadServiceInfo = useReloadServiceInfo()
const [showSettings, setShowSettings] = useState(false)
const [alert, setAlert] = useState(null)
const [serviceInfoAlert, setServiceInfoAlert] = useState(null)
const [isLoading, setIsLoading] = useState(true)
@ -472,24 +472,11 @@ export default function Earn({ wallet }) {
{!serviceInfo?.coinjoinInProgress && (
<Formik initialValues={initialValues} validate={validate} onSubmit={onSubmit}>
{({ handleSubmit, setFieldValue, handleChange, handleBlur, values, touched, errors, isSubmitting }) => (
<rb.Form onSubmit={handleSubmit} noValidate>
{!serviceInfo?.makerRunning && !isWaitingMakerStart && !isWaitingMakerStop && (
<div className={styles['settings-container']}>
<rb.Button
variant={`${settings.theme}`}
className={`${styles['settings-btn']} d-flex align-items-center`}
onClick={() => setShowSettings((current) => !current)}
>
{t('earn.button_settings')}
<Sprite
symbol={`caret-${showSettings ? 'up' : 'down'}`}
className="ms-1"
width="20"
height="20"
/>
</rb.Button>
{showSettings && (
<div className="my-4">
<>
<rb.Form onSubmit={handleSubmit} noValidate>
{!serviceInfo?.makerRunning && !isWaitingMakerStart && !isWaitingMakerStop && (
<Accordion title={t('earn.button_settings')}>
<>
<rb.Form.Group className="mb-4 d-flex justify-content-center" controlId="offertype">
<SegmentedTabs
name="offertype"
@ -518,16 +505,16 @@ export default function Earn({ wallet }) {
typeof values.feeRel === 'number' ? `(${factorToPercentage(values.feeRel)}%)` : '',
})}
</rb.Form.Label>
<div className="mb-2">
<rb.Form.Text className="text-secondary">{t('earn.description_rel_fee')}</rb.Form.Text>
</div>
<rb.Form.Text className="d-block text-secondary mb-2">
{t('earn.description_rel_fee')}
</rb.Form.Text>
{isLoading ? (
<rb.Placeholder as="div" animation="wave">
<rb.Placeholder xs={12} className={styles['input-loader']} />
</rb.Placeholder>
) : (
<rb.InputGroup>
<rb.InputGroup.Text id="feeRel-addon1" className={styles['input-group-text']}>
<rb.InputGroup hasValidation>
<rb.InputGroup.Text id="feeRel-addon1" className={styles.inputGroupText}>
%
</rb.InputGroup.Text>
<rb.Form.Control
@ -543,13 +530,13 @@ export default function Earn({ wallet }) {
onBlur={handleBlur}
value={typeof values.feeRel === 'number' ? factorToPercentage(values.feeRel) : ''}
isValid={touched.feeRel && !errors.feeRel}
isInvalid={touched.feeRel && errors.feeRel}
isInvalid={touched.feeRel && !!errors.feeRel}
min={0}
step={feeRelPercentageStep}
/>
<rb.Form.Control.Feedback type="invalid">{errors.feeRel}</rb.Form.Control.Feedback>
</rb.InputGroup>
)}
<rb.Form.Control.Feedback type="invalid">{errors.feeRel}</rb.Form.Control.Feedback>
</rb.Form.Group>
) : (
<rb.Form.Group className="mb-3" controlId="feeAbs">
@ -561,16 +548,16 @@ export default function Earn({ wallet }) {
: '',
})}
</rb.Form.Label>
<div className="mb-2">
<rb.Form.Text className="text-secondary">{t('earn.description_abs_fee')}</rb.Form.Text>
</div>
<rb.Form.Text className="d-block text-secondary mb-2">
{t('earn.description_abs_fee')}
</rb.Form.Text>
{isLoading ? (
<rb.Placeholder as="div" animation="wave">
<rb.Placeholder xs={12} className={styles['input-loader']} />
</rb.Placeholder>
) : (
<rb.InputGroup>
<rb.InputGroup.Text id="feeAbs-addon1" className={styles['input-group-text']}>
<rb.InputGroup hasValidation>
<rb.InputGroup.Text id="feeAbs-addon1" className={styles.inputGroupText}>
<Sprite symbol="sats" width="24" height="24" />
</rb.InputGroup.Text>
<rb.Form.Control
@ -583,13 +570,13 @@ export default function Earn({ wallet }) {
onChange={handleChange}
onBlur={handleBlur}
isValid={touched.feeAbs && !errors.feeAbs}
isInvalid={touched.feeAbs && errors.feeAbs}
isInvalid={touched.feeAbs && !!errors.feeAbs}
min={0}
step={1}
/>
<rb.Form.Control.Feedback type="invalid">{errors.feeAbs}</rb.Form.Control.Feedback>
</rb.InputGroup>
)}
<rb.Form.Control.Feedback type="invalid">{errors.feeAbs}</rb.Form.Control.Feedback>
</rb.Form.Group>
)}
@ -600,8 +587,8 @@ export default function Earn({ wallet }) {
<rb.Placeholder xs={12} className={styles['input-loader']} />
</rb.Placeholder>
) : (
<rb.InputGroup>
<rb.InputGroup.Text id="minsize-addon1" className={styles['input-group-text']}>
<rb.InputGroup hasValidation>
<rb.InputGroup.Text id="minsize-addon1" className={styles.inputGroupText}>
<Sprite symbol="sats" width="24" height="24" />
</rb.InputGroup.Text>
<rb.Form.Control
@ -617,46 +604,44 @@ export default function Earn({ wallet }) {
min={0}
step={1000}
/>
<rb.Form.Control.Feedback type="invalid">{errors.minsize}</rb.Form.Control.Feedback>
</rb.InputGroup>
)}
<rb.Form.Control.Feedback type="invalid">{errors.minsize}</rb.Form.Control.Feedback>
</rb.Form.Group>
</>
</Accordion>
)}
<div className="mt-4">
<rb.Button
variant="dark"
type="submit"
className={styles['earn-btn']}
disabled={isLoading || isSubmitting || isWaitingMakerStart || isWaitingMakerStop}
>
<div className="d-flex justify-content-center align-items-center">
{(isWaitingMakerStart || isWaitingMakerStop) && (
<rb.Spinner
as="span"
animation="border"
size="sm"
role="status"
aria-hidden="true"
className="me-2"
/>
)}
{isWaitingMakerStart || isWaitingMakerStop ? (
<>
{isWaitingMakerStart && t('earn.text_starting')}
{isWaitingMakerStop && t('earn.text_stopping')}
</>
) : (
<>{serviceInfo?.makerRunning === true ? t('earn.button_stop') : t('earn.button_start')}</>
)}
</div>
)}
<hr className="m-0" />
</rb.Button>
</div>
)}
<div className="mt-4">
<rb.Button
variant="dark"
type="submit"
className={styles['earn-btn']}
disabled={isLoading || isSubmitting || isWaitingMakerStart || isWaitingMakerStop}
>
<div className="d-flex justify-content-center align-items-center">
{(isWaitingMakerStart || isWaitingMakerStop) && (
<rb.Spinner
as="span"
animation="border"
size="sm"
role="status"
aria-hidden="true"
className="me-2"
/>
)}
{isWaitingMakerStart || isWaitingMakerStop ? (
<>
{isWaitingMakerStart && t('earn.text_starting')}
{isWaitingMakerStop && t('earn.text_stopping')}
</>
) : (
<>{serviceInfo?.makerRunning === true ? t('earn.button_stop') : t('earn.button_start')}</>
)}
</div>
</rb.Button>
</div>
</rb.Form>
</rb.Form>
</>
)}
</Formik>
)}

View file

@ -17,19 +17,7 @@
width: 100%;
}
.settings-container {
margin-top: 1.5rem;
}
.settings-container button.settings-btn {
background-color: transparent !important;
border: none;
padding-left: 0;
height: 3rem;
width: 100%;
}
.settings-container .input-group-text {
.inputGroupText {
width: 5ch;
display: inline-flex;
justify-content: center;

View file

@ -87,7 +87,7 @@ const withTooltip = (node: React.ReactElement, tooltip: string) => {
// `TableNode` is known to have same properties as `ObwatchApi.Order`, hence prefer casting over object destructuring
const toOrder = (tableNode: TableTypes.TableNode) => tableNode as unknown as ObwatchApi.Order
const renderOrderType = (val: string, t: TFunction<'translation', undefined>) => {
const renderOrderType = (val: string, t: TFunction) => {
if (val === ObwatchApi.ABSOLUTE_ORDER_TYPE_VAL) {
return withTooltip(<rb.Badge bg="info">{t('orderbook.text_offer_type_absolute')}</rb.Badge>, val)
}

View file

@ -12,6 +12,7 @@ import { CopyButton } from './CopyButton'
import { ShareButton, checkIsWebShareAPISupported } from './ShareButton'
import { SelectableJar, jarFillLevel } from './jars/Jar'
import styles from './Receive.module.css'
import Accordion from './Accordion'
export default function Receive({ wallet }) {
const { t } = useTranslation()
@ -25,7 +26,6 @@ export default function Receive({ wallet }) {
const [amount, setAmount] = useState('')
const [selectedJarIndex, setSelectedJarIndex] = useState(parseInt(location.state?.account, 10) || 0)
const [addressCount, setAddressCount] = useState(0)
const [showSettings, setShowSettings] = useState(false)
const sortedAccountBalances = useMemo(() => {
if (!walletInfo) return []
@ -105,66 +105,53 @@ export default function Receive({ wallet }) {
</rb.Card>
</div>
<rb.Form onSubmit={onSubmit} validated={validated} noValidate>
<div className={styles['settings-container']}>
<rb.Button
variant={`${settings.theme}`}
className={`${styles['settings-btn']} d-flex align-items-center`}
onClick={() => setShowSettings((current) => !current)}
>
{t('receive.button_settings')}
<Sprite symbol={`caret-${showSettings ? 'up' : 'down'}`} className="ms-1" width="20" height="20" />
</rb.Button>
{showSettings && (
<div className="my-4">
{!walletInfo || sortedAccountBalances.length === 0 ? (
<rb.Placeholder as="div" animation="wave">
<rb.Placeholder className={styles.jarsPlaceholder} />
</rb.Placeholder>
) : (
<div className={styles.jarsContainer}>
{sortedAccountBalances.map((it) => (
<SelectableJar
key={it.accountIndex}
index={it.accountIndex}
balance={it.calculatedTotalBalanceInSats}
isSelectable={true}
isSelected={it.accountIndex === selectedJarIndex}
fillLevel={jarFillLevel(
it.calculatedTotalBalanceInSats,
walletInfo.balanceSummary.calculatedTotalBalanceInSats
)}
onClick={(jarIndex) => setSelectedJarIndex(jarIndex)}
/>
))}
</div>
)}
<rb.Form.Group controlId="amountSats">
<rb.Form.Label>{t('receive.label_amount')}</rb.Form.Label>
<rb.InputGroup>
<rb.InputGroup.Text id="amountSats-addon1" className={styles.inputGroupText}>
<Sprite symbol="sats" width="24" height="24" />
</rb.InputGroup.Text>
<rb.Form.Control
aria-label={t('receive.label_amount')}
className="slashed-zeroes"
name="amount"
type="number"
placeholder="0"
value={amount}
disabled={isLoading}
onChange={(e) => setAmount(e.target.value)}
min={0}
step={1}
<Accordion title={t('receive.button_settings')}>
<div>
{!walletInfo || sortedAccountBalances.length === 0 ? (
<rb.Placeholder as="div" animation="wave">
<rb.Placeholder className={styles.jarsPlaceholder} />
</rb.Placeholder>
) : (
<div className={styles.jarsContainer}>
{sortedAccountBalances.map((it) => (
<SelectableJar
key={it.accountIndex}
index={it.accountIndex}
balance={it.calculatedTotalBalanceInSats}
isSelectable={true}
isSelected={it.accountIndex === selectedJarIndex}
fillLevel={jarFillLevel(
it.calculatedTotalBalanceInSats,
walletInfo.balanceSummary.calculatedTotalBalanceInSats
)}
onClick={(jarIndex) => setSelectedJarIndex(jarIndex)}
/>
</rb.InputGroup>
<rb.Form.Control.Feedback type="invalid">
{t('receive.feedback_invalid_amount')}
</rb.Form.Control.Feedback>
</rb.Form.Group>
</div>
)}
<hr className="m-0" />
</div>
))}
</div>
)}
<rb.Form.Group controlId="amountSats">
<rb.Form.Label>{t('receive.label_amount')}</rb.Form.Label>
<rb.InputGroup>
<rb.InputGroup.Text id="amountSats-addon1" className={styles.inputGroupText}>
<Sprite symbol="sats" width="24" height="24" />
</rb.InputGroup.Text>
<rb.Form.Control
aria-label={t('receive.label_amount')}
className="slashed-zeroes"
name="amount"
type="number"
placeholder="0"
value={amount}
disabled={isLoading}
onChange={(e) => setAmount(e.target.value)}
min={0}
step={1}
/>
</rb.InputGroup>
<rb.Form.Control.Feedback type="invalid">{t('receive.feedback_invalid_amount')}</rb.Form.Control.Feedback>
</rb.Form.Group>
</div>
</Accordion>
<div className="mt-4 d-flex justify-content-center">
<rb.Button

View file

@ -14,18 +14,6 @@
border-color: rgba(222, 222, 222, 1);
}
.settings-container {
margin-top: 1.5rem;
}
.settings-container button.settings-btn {
background-color: transparent !important;
border: none;
padding-left: 0;
height: 3rem;
width: 100%;
}
.receive form input {
height: 3.5rem;
width: 100%;

View file

@ -38,65 +38,62 @@ const CollaboratorsSelector = ({
}
return (
// @ts-ignore FIXME: "Property 'disabled' does not exist on type..."
<rb.Form noValidate className={styles.collaboratorsSelector} disabled={disabled}>
<rb.Form.Group>
<rb.Form.Label className="mb-0">{t('send.label_num_collaborators', { numCollaborators })}</rb.Form.Label>
<div className="mb-2">
<rb.Form.Text className="text-secondary">{t('send.description_num_collaborators')}</rb.Form.Text>
</div>
<div className="d-flex flex-row flex-wrap gap-2">
{defaultCollaboratorsSelection.map((number) => {
const isSelected = !usesCustomNumCollaborators && numCollaborators === number
return (
<rb.Button
key={number}
variant={settings.theme === 'light' ? 'white' : 'dark'}
className={classNames(styles.collaboratorsSelectorElement, 'border', 'border-1', {
[styles.selected]: isSelected,
})}
onClick={() => {
setUsesCustomNumCollaborators(false)
setNumCollaborators(number)
}}
disabled={disabled}
>
{number}
</rb.Button>
)
<rb.Form.Group className={styles.collaboratorsSelector}>
<rb.Form.Label className="mb-0">{t('send.label_num_collaborators', { numCollaborators })}</rb.Form.Label>
<div className="mb-2">
<rb.Form.Text className="text-secondary">{t('send.description_num_collaborators')}</rb.Form.Text>
</div>
<div className="d-flex flex-row flex-wrap gap-2">
{defaultCollaboratorsSelection.map((number) => {
const isSelected = !usesCustomNumCollaborators && numCollaborators === number
return (
<rb.Button
key={number}
variant={settings.theme === 'light' ? 'white' : 'dark'}
className={classNames(styles.collaboratorsSelectorElement, 'border', 'border-1', {
[styles.selected]: isSelected,
})}
onClick={() => {
setUsesCustomNumCollaborators(false)
setNumCollaborators(number)
}}
disabled={disabled}
>
{number}
</rb.Button>
)
})}
<rb.Form.Control
type="number"
min={minNumCollaborators}
max={99}
isInvalid={!isValidNumCollaborators(numCollaborators, minNumCollaborators)}
placeholder={t('send.input_num_collaborators_placeholder')}
defaultValue=""
className={classNames(styles.collaboratorsSelectorElement, 'border', 'border-1', {
[styles.selected]: usesCustomNumCollaborators,
})}
<rb.Form.Control
type="number"
min={minNumCollaborators}
max={99}
isInvalid={!isValidNumCollaborators(numCollaborators, minNumCollaborators)}
placeholder={t('send.input_num_collaborators_placeholder')}
defaultValue=""
className={classNames(styles.collaboratorsSelectorElement, 'border', 'border-1', {
[styles.selected]: usesCustomNumCollaborators,
})}
onChange={(e) => {
onChange={(e) => {
setUsesCustomNumCollaborators(true)
validateAndSetCustomNumCollaborators(e.target.value)
}}
onClick={(e) => {
// @ts-ignore - FIXME: "Property 'value' does not exist on type 'EventTarget'"
if (e.target.value !== '') {
setUsesCustomNumCollaborators(true)
validateAndSetCustomNumCollaborators(e.target.value)
}}
onClick={(e) => {
// @ts-ignore - FIXME: "Property 'value' does not exist on type 'EventTarget'"
if (e.target.value !== '') {
setUsesCustomNumCollaborators(true)
// @ts-ignore - FIXME: "Property 'value' does not exist on type 'EventTarget'"
validateAndSetCustomNumCollaborators(e.target.value)
}
}}
disabled={disabled}
/>
{usesCustomNumCollaborators && (
<rb.Form.Control.Feedback type="invalid">
{t('send.error_invalid_num_collaborators', { minNumCollaborators, maxNumCollaborators: 99 })}
</rb.Form.Control.Feedback>
)}
</div>
</rb.Form.Group>
</rb.Form>
validateAndSetCustomNumCollaborators(e.target.value)
}
}}
disabled={disabled}
/>
{usesCustomNumCollaborators && (
<rb.Form.Control.Feedback type="invalid">
{t('send.error_invalid_num_collaborators', { minNumCollaborators, maxNumCollaborators: 99 })}
</rb.Form.Control.Feedback>
)}
</div>
</rb.Form.Group>
)
}

View file

@ -35,6 +35,7 @@ import {
isValidJarIndex,
isValidNumCollaborators,
} from './helpers'
import Accordion from '../Accordion'
const IS_COINJOIN_DEFAULT_VAL = true
// initial value for `minimum_makers` from the default joinmarket.cfg (last check on 2022-02-20 of v0.9.5)
@ -602,19 +603,16 @@ export default function Send({ wallet }: SendProps) {
)}
</>
</rb.Fade>
{alert && (
<rb.Alert className="slashed-zeroes" variant={alert.variant}>
{alert.message}
</rb.Alert>
)}
{paymentSuccessfulInfoAlert && (
<rb.Alert className="small slashed-zeroes break-word" variant={paymentSuccessfulInfoAlert.variant}>
{paymentSuccessfulInfoAlert.message}
</rb.Alert>
)}
{!isLoading && !isOperationDisabled && isCoinjoin && !coinjoinPreconditionSummary.isFulfilled && (
<div className="mb-4">
<CoinjoinPreconditionViolationAlert
@ -623,7 +621,6 @@ export default function Send({ wallet }: SendProps) {
/>
</div>
)}
{!isLoading && walletInfo && (
<JarSelectorModal
isShown={destinationJarPickerShown}
@ -657,7 +654,6 @@ export default function Send({ wallet }: SendProps) {
}}
/>
)}
<rb.Form id="send-form" onSubmit={onSubmit} noValidate className={styles['send-form']}>
<rb.Form.Group className="mb-4 flex-grow-1" controlId="sourceJarIndex">
<rb.Form.Label>{t('send.label_source_jar')}</rb.Form.Label>
@ -805,24 +801,26 @@ export default function Send({ wallet }: SendProps) {
</rb.Form.Control.Feedback>
{isSweep && frozenOrLockedWarning()}
</rb.Form.Group>
<rb.Form.Group controlId="isCoinjoin" className={`${isCoinjoin ? 'mb-3' : ''}`}>
<ToggleSwitch
label={t('send.toggle_coinjoin')}
subtitle={t('send.toggle_coinjoin_subtitle')}
toggledOn={isCoinjoin}
onToggle={(isToggled) => setIsCoinjoin(isToggled)}
disabled={isLoading || isOperationDisabled}
/>
</rb.Form.Group>
<Accordion title={t('send.sending_options')}>
<rb.Form.Group controlId="isCoinjoin" className={`${isCoinjoin ? 'mb-3' : ''}`}>
<ToggleSwitch
label={t('send.toggle_coinjoin')}
subtitle={t('send.toggle_coinjoin_subtitle')}
toggledOn={isCoinjoin}
onToggle={(isToggled) => setIsCoinjoin(isToggled)}
disabled={isLoading || isOperationDisabled}
/>
</rb.Form.Group>
<div className={isCoinjoin ? 'd-block' : 'd-none'}>
<CollaboratorsSelector
numCollaborators={numCollaborators}
setNumCollaborators={setNumCollaborators}
minNumCollaborators={minNumCollaborators}
disabled={isLoading || isOperationDisabled}
/>
</div>
</Accordion>
</rb.Form>
{isCoinjoin && (
<CollaboratorsSelector
numCollaborators={numCollaborators}
setNumCollaborators={setNumCollaborators}
minNumCollaborators={minNumCollaborators}
disabled={isLoading || isOperationDisabled}
/>
)}
<rb.Button
ref={submitButtonRef}
variant={submitButtonOptions.variant}
@ -840,7 +838,6 @@ export default function Send({ wallet }: SendProps) {
<>{submitButtonOptions.text}</>
)}
</rb.Button>
{showConfirmAbortModal && (
<ConfirmModal
isShown={showConfirmAbortModal}
@ -851,7 +848,6 @@ export default function Send({ wallet }: SendProps) {
{t('send.confirm_abort_modal.text_body')}
</ConfirmModal>
)}
{showConfirmSendModal && (
<PaymentConfirmModal
isShown={true}

View file

@ -41,7 +41,7 @@ interface Result {
mustReload: boolean
}
const errorResolver = (t: TFunction<'translation', undefined>, i18nKey: string | string[]) => ({
const errorResolver = (t: TFunction, i18nKey: string | string[]) => ({
resolver: (_: Response, reason: string) => `${t(i18nKey)} ${reason}`,
fallbackReason: t('global.errors.reason_unknown'),
})

View file

@ -232,6 +232,7 @@
"label_amount": "Amount in sats",
"placeholder_amount": "Enter amount...",
"feedback_invalid_amount": "Please provide a valid amount.",
"sending_options": "Sending options",
"toggle_coinjoin": "Send as collaborative transaction",
"toggle_coinjoin_subtitle": "Collaborative transactions improve the privacy of yourself and others.",
"button_send": "Send",

View file

@ -195,6 +195,7 @@
"label_amount": "Montant en sats",
"placeholder_amount": "Entrez le montant...",
"feedback_invalid_amount": "Veuillez fournir un montant valide.",
"sending_options": "Options d'envoi",
"toggle_coinjoin": "Envoyer en tant que transaction collaborative pour une meilleure confidentialité",
"button_send": "Envoyer",
"text_sending": "Envoi de ",