Repository: joinmarket-webui/joinmarket-webui
Pull request #222 (2022-04-18 13:05:34)

ui: small visual adaptions after placeholder PR
Some small stuff that has been noticed after #203 has been merged.

- add border-radius to input field placeholder
- align loading spinner in submit buttons on `Send` and `Receive` screen
- re-add initial collaborative transaction toggle state on Send screen
- restore send form field order ("account to send from" is at the top)
- avoid flickering on receive page (still not sure if the placeholder for the qr code should be displayed, when an address is already present; there is a case for showing it just for the initial loading phase)
- re-add icons on send/receive buttons in `CurrentWalletMagic`

Findings: When the initial page load is the `Send` page, the "collaborative transaction" toggle is not enabled - this should be fixed after is #218 rebased. 

Edit: Uff.. these placeholders are insanely resource intensive.. - my fans spin up if they are displayed for a while (e.g. when token became invalid).


Patch:
diff --git a/public/sprite.svg b/public/sprite.svg
index eb7dcae..ba276da 100644
--- a/public/sprite.svg
+++ b/public/sprite.svg
@@ -44,16 +44,16 @@
         <path d="M8 9.5a2 2 0 1 0 0-4 2 2 0 0 0 0 4z"/>
         <path d="M9.5 2c-.9 0-1.75.216-2.501.6A5 5 0 0 1 13 7.5a6.5 6.5 0 1 1-13 0 .5.5 0 0 1 1 0 5.5 5.5 0 0 0 8.001 4.9A5 5 0 0 1 3 7.5a6.5 6.5 0 0 1 13 0 .5.5 0 0 1-1 0A5.5 5.5 0 0 0 9.5 2zM8 3.5a4 4 0 1 0 0 8 4 4 0 0 0 0-8z"/>
     </symbol>
-    <symbol xmlns="http://www.w3.org/2000/svg" id="welcome" aria-hidden="true" class="svg-inline--fa fa-door-open fa-w-20" data-icon="door-open" data-prefix="fas" focusable="false" role="img" viewBox="0 0 640 512">
+    <symbol id="welcome" viewBox="0 0 640 512">
         <path fill="currentColor" d="M624 448h-80V113.45C544 86.19 522.47 64 496 64H384v64h96v384h144c8.84 0 16-7.16 16-16v-32c0-8.84-7.16-16-16-16zM312.24 1.01l-192 49.74C105.99 54.44 96 67.7 96 82.92V448H16c-8.84 0-16 7.16-16 16v32c0 8.84 7.16 16 16 16h336V33.18c0-21.58-19.56-37.41-39.76-32.17zM264 288c-13.25 0-24-14.33-24-32s10.75-32 24-32 24 14.33 24 32-10.75 32-24 32z"/>
     </symbol>
-    <symbol xmlns="http://www.w3.org/2000/svg" id="collab" aria-hidden="true" class="svg-inline--fa fa-compress-arrows-alt fa-w-16" data-icon="compress-arrows-alt" data-prefix="fas" focusable="false" role="img" viewBox="0 0 512 512">
+    <symbol id="collab" viewBox="0 0 512 512">
         <path fill="currentColor" d="M200 288H88c-21.4 0-32.1 25.8-17 41l32.9 31-99.2 99.3c-6.2 6.2-6.2 16.4 0 22.6l25.4 25.4c6.2 6.2 16.4 6.2 22.6 0L152 408l31.1 33c15.1 15.1 40.9 4.4 40.9-17V312c0-13.3-10.7-24-24-24zm112-64h112c21.4 0 32.1-25.9 17-41l-33-31 99.3-99.3c6.2-6.2 6.2-16.4 0-22.6L481.9 4.7c-6.2-6.2-16.4-6.2-22.6 0L360 104l-31.1-33C313.8 55.9 288 66.6 288 88v112c0 13.3 10.7 24 24 24zm96 136l33-31.1c15.1-15.1 4.4-40.9-17-40.9H312c-13.3 0-24 10.7-24 24v112c0 21.4 25.9 32.1 41 17l31-32.9 99.3 99.3c6.2 6.2 16.4 6.2 22.6 0l25.4-25.4c6.2-6.2 6.2-16.4 0-22.6L408 360zM183 71.1L152 104 52.7 4.7c-6.2-6.2-16.4-6.2-22.6 0L4.7 30.1c-6.2 6.2-6.2 16.4 0 22.6L104 152l-33 31.1C55.9 198.2 66.6 224 88 224h112c13.3 0 24-10.7 24-24V88c0-21.3-25.9-32-41-16.9z"/>
     </symbol>
-    <symbol xmlns="http://www.w3.org/2000/svg" id="key" fill="currentColor" data-v-4fa90e7f="" viewBox="0 0 24 24">
+    <symbol id="key" fill="currentColor" viewBox="0 0 24 24">
         <path fill-rule="evenodd" d="M18.865 7.374c.74 2.968-1.163 5.998-4.25 6.767a6.01 6.01 0 01-1.155.172l-3.435 5.471a2 2 0 01-1.21.877l-1.974.492a1 1 0 01-1.212-.728l-.445-1.786a2 2 0 01.246-1.547l3.157-5.028a5.338 5.338 0 01-.9-1.903c-.74-2.968 1.162-5.998 4.249-6.768 3.087-.77 6.189 1.013 6.929 3.98zm-4.627 1.324c.685-.171 1.108-.844.944-1.504-.165-.66-.854-1.055-1.54-.884-.686.17-1.109.844-.944 1.503.164.66.854 1.056 1.54.885z" clip-rule="evenodd"/>
     </symbol>
-    <symbol xmlns="http://www.w3.org/2000/svg" id="handshake" aria-hidden="true" class="svg-inline--fa fa-handshake-slash fa-w-20" data-icon="handshake-slash" data-prefix="fas" focusable="false" role="img" viewBox="0 0 640 512">
+    <symbol id="handshake" viewBox="0 0 640 512">
         <path fill="currentColor" d="M0,128.21V384H64a32,32,0,0,0,32-32V184L23.83,128.21ZM48,320.1a16,16,0,1,1-16,16A16,16,0,0,1,48,320.1Zm80,31.81h18.3l90.5,81.89a64,64,0,0,0,90-9.3l.2-.2,17.91,15.5a37.16,37.16,0,0,0,52.29-5.39l8.8-10.82L128,208.72Zm416-223.7V352.1a32,32,0,0,0,32,32h64V128.21ZM592,352.1a16,16,0,1,1,16-16A16,16,0,0,1,592,352.1ZM303.33,202.67l59.58-54.57a16,16,0,0,1,21.59,23.61L358.41,195.6,504,313.8a73.08,73.08,0,0,1,7.91,7.7V128L457.3,73.41A31.76,31.76,0,0,0,434.7,64H348.8a31.93,31.93,0,0,0-21.6,8.41l-88.07,80.64-25.64-19.81L289.09,64H205.3a32,32,0,0,0-22.6,9.41L162.36,93.72,45.47,3.38A16,16,0,0,0,23,6.19L3.38,31.46A16,16,0,0,0,6.19,53.91L594.53,508.63A16,16,0,0,0,617,505.82l19.65-25.27a16,16,0,0,0-2.82-22.45Z"/>
     </symbol>
     <symbol id="shield-outline" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -62,7 +62,7 @@
     <symbol id="shield-filled" fill="currentColor" viewBox="0 0 24 24">
         <path d="M5 7c0-.276.225-.499.498-.535 2.149-.28 5.282-2.186 6.224-2.785a.516.516 0 01.556 0c.942.599 4.075 2.504 6.224 2.785.273.036.498.259.498.535v4.75c0 6.5-7 8.75-7 8.75s-7-2.25-7-8.75V7z"/>
     </symbol>
-    <symbol id="shield-filled-loading" fill="#e9ecef" viewBox="0 0 24 24">
+    <symbol id="shield-filled-loading" fill="currentColor" viewBox="0 0 24 24">
         <path d="M5 7c0-.276.225-.499.498-.535 2.149-.28 5.282-2.186 6.224-2.785a.516.516 0 01.556 0c.942.599 4.075 2.504 6.224 2.785.273.036.498.259.498.535v4.75c0 6.5-7 8.75-7 8.75s-7-2.25-7-8.75V7z"/>
     </symbol>
     <symbol id="wand" fill="currentColor" viewBox="0 0 24 24">
@@ -81,7 +81,7 @@
     <symbol id="caret-down" fill="currentColor" viewBox="0 0 24 24">
         <path fill-rule="evenodd" d="M4.47 9.4a.75.75 0 011.06 0l6.364 6.364a.25.25 0 00.354 0L18.612 9.4a.75.75 0 011.06 1.06l-6.364 6.364a1.75 1.75 0 01-2.475 0L4.47 10.46a.75.75 0 010-1.06z" clip-rule="evenodd"/>
     </symbol>
-    <symbol xmlns="http://www.w3.org/2000/svg" id="mnemonic" fill="currentColor" viewBox="0 0 24 24">
+    <symbol id="mnemonic" fill="currentColor" viewBox="0 0 24 24">
         <path fill-rule="evenodd" d="M5 6.5A1.5 1.5 0 016.5 5h11A1.5 1.5 0 0119 6.5v11a1.5 1.5 0 01-1.5 1.5h-11A1.5 1.5 0 015 17.5v-11zm1.5 0H9V8H6.5V6.5zM9 9.667H6.5v1.5H9v-1.5zm-2.5 3.166H9v1.5H6.5v-1.5zM9 16H6.5v1.5H9V16zm1.75-9.5h2.5V8h-2.5V6.5zm2.5 3.167h-2.5v1.5h2.5v-1.5zm-2.5 3.166h2.5v1.5h-2.5v-1.5zM13.25 16h-2.5v1.5h2.5V16zM15 6.5h2.5V8H15V6.5zm2.5 3.167H15v1.5h2.5v-1.5zM15 12.833h2.5v1.5H15v-1.5zM17.5 16H15v1.5h2.5V16z" clip-rule="evenodd"/>
     </symbol>
     <symbol id="globe" fill="currentColor" viewBox="0 0 24 24">
diff --git a/src/components/Balance.jsx b/src/components/Balance.jsx
index b86cf98..cc7b002 100644
--- a/src/components/Balance.jsx
+++ b/src/components/Balance.jsx
@@ -45,15 +45,7 @@ const formatSats = (value) => {
   return formatter.format(value)
 }
 
-const BalanceComponent = ({ symbol, value, symbolIsPrefix, loading }) => {
-  if (loading) {
-    return (
-      <rb.Placeholder as="p" animation="wave" className="mb-0">
-        <rb.Placeholder className={styles['balance-component-placeholder']} />
-      </rb.Placeholder>
-    )
-  }
-
+const BalanceComponent = ({ symbol, value, symbolIsPrefix }) => {
   return (
     <span className="d-inline-flex align-items-center">
       {symbolIsPrefix && symbol}
@@ -75,15 +67,27 @@ const BalanceComponent = ({ symbol, value, symbolIsPrefix, loading }) => {
  * Possible options are `BTC` and `SATS` from `src/utils.js`
  * @param {showBalance}: A flag indicating whether to render or hide the balance.
  * Hidden balances are masked with `*****`.
+ * @param {loading}: A loading flag that renders a placeholder while true.
  */
-export default function Balance({ valueString, convertToUnit, showBalance = false, loading }) {
+export default function Balance({ valueString, convertToUnit, showBalance = false, loading = false }) {
   const [displayMode, setDisplayMode] = useState(DISPLAY_MODE_HIDDEN)
 
   useEffect(() => {
     setDisplayMode(getDisplayMode(convertToUnit, showBalance))
   }, [convertToUnit, showBalance])
 
-  if (displayMode === DISPLAY_MODE_HIDDEN && !loading) {
+  if (loading) {
+    return (
+      <rb.Placeholder as="div" animation="wave">
+        <rb.Placeholder
+          data-testid="balance-component-placeholder"
+          className={styles['balance-component-placeholder']}
+        />
+      </rb.Placeholder>
+    )
+  }
+
+  if (displayMode === DISPLAY_MODE_HIDDEN) {
     return (
       <BalanceComponent
         symbol={
@@ -98,8 +102,8 @@ export default function Balance({ valueString, convertToUnit, showBalance = fals
   }
 
   if (typeof valueString !== 'string') {
-    if (!loading) console.warn('<Balance /> component expects string input')
-    return <BalanceComponent symbol="" value={valueString} symbolIsPrefix={false} loading={loading} />
+    console.warn('<Balance /> component expects string input')
+    return <BalanceComponent symbol="" value={valueString} symbolIsPrefix={false} />
   }
 
   // Treat integers as sats.
diff --git a/src/components/CurrentWalletMagic.jsx b/src/components/CurrentWalletMagic.jsx
index 52988a4..454d572 100644
--- a/src/components/CurrentWalletMagic.jsx
+++ b/src/components/CurrentWalletMagic.jsx
@@ -14,19 +14,19 @@ const WalletHeader = ({ name, balance, unit, showBalance, loading }) => {
   return (
     <div className="d-flex flex-column align-items-center">
       {loading && (
-        <rb.Placeholder as="p" animation="wave" className="mb-0">
-          <rb.Placeholder className={styles['wallet-header-title']} />
+        <rb.Placeholder as="div" animation="wave">
+          <rb.Placeholder className={styles['wallet-header-title-placeholder']} />
         </rb.Placeholder>
       )}
       {!loading && <h1 className="text-secondary fs-6">{walletDisplayName(name)}</h1>}
       {loading && (
-        <rb.Placeholder as="p" animation="wave" className="mb-0">
-          <rb.Placeholder className={styles['wallet-header-subtitle']} />
+        <rb.Placeholder as="div" animation="wave">
+          <rb.Placeholder className={styles['wallet-header-subtitle-placeholder']} />
         </rb.Placeholder>
       )}
       {!loading && (
         <h2>
-          <Balance valueString={balance} convertToUnit={unit} showBalance={showBalance || false} loading={loading} />
+          <Balance valueString={balance} convertToUnit={unit} showBalance={showBalance || false} />
         </h2>
       )}
     </div>
@@ -34,23 +34,26 @@ const WalletHeader = ({ name, balance, unit, showBalance, loading }) => {
 }
 
 const PrivacyLevels = ({ accounts, loading }) => {
-  const sortedAccounts = accounts?.sort((lhs, rhs) => lhs.account - rhs.account)
-  const numAccounts = sortedAccounts?.length
+  const numPrivacyLevelsPalceholders = 5
+  const sortedAccounts = (accounts || []).sort((lhs, rhs) => lhs.account - rhs.account)
+  const numAccounts = sortedAccounts.length
 
   return (
     <div className="d-flex justify-content-center">
       <div className="d-flex flex-column align-items-start" style={{ gap: '1rem' }}>
-        {loading && [...new Array(5)].map((a, index) => <LoadingPrivacyLevel key={index} level={5} />)}
-        {!loading &&
-          sortedAccounts.map(({ account, account_balance: balance, branches }) => (
-            <PrivacyLevel
-              key={account}
-              numAccounts={numAccounts}
-              level={parseInt(account)}
-              balance={balance}
-              loading={loading}
-            />
-          ))}
+        {loading
+          ? Array(numPrivacyLevelsPalceholders)
+              .fill('')
+              .map((_, index) => <LoadingPrivacyLevel key={index} level={numPrivacyLevelsPalceholders} />)
+          : sortedAccounts.map(({ account, account_balance: balance }) => (
+              <PrivacyLevel
+                key={account}
+                numAccounts={numAccounts}
+                level={parseInt(account)}
+                balance={balance}
+                loading={loading}
+              />
+            ))}
       </div>
     </div>
   )
@@ -67,13 +70,13 @@ const LoadingPrivacyLevel = ({ level }) => {
     <div className="d-flex align-items-center">
       <div className="d-flex">{loadingShields}</div>
       <div className="ps-2">
-        <Balance valueString={undefined} convertToUnit={undefined} showBalance={undefined} loading={true} />
+        <Balance loading={true} />
       </div>
     </div>
   )
 }
 
-const PrivacyLevel = ({ numAccounts, level, balance, loading }) => {
+const PrivacyLevel = ({ numAccounts, level, balance }) => {
   const settings = useSettings()
 
   const filledShields = Array(level + 1)
@@ -147,22 +150,28 @@ export default function CurrentWalletMagic() {
           />
         </rb.Row>
         <rb.Row className="mt-4">
+          <rb.Col>
+            {/* Todo: Withdrawing needs to factor in the privacy levels as well.
+              Depending on the mixdepth/account there will be different amounts available. */}
+            <ExtendedLink to={routes.send} className="btn btn-outline-dark w-100" disabled={isLoading}>
+              <div className="d-flex justify-content-center align-items-center">
+                <Sprite symbol="send" width="24" height="24" />
+                <div className="ps-1">{t('current_wallet.button_withdraw')}</div>
+              </div>
+            </ExtendedLink>
+          </rb.Col>
           <rb.Col>
             {/* Always receive on first mixdepth. */}
             <ExtendedLink
-              disabled={isLoading}
               to={routes.receive}
               state={{ account: 0 }}
               className="btn btn-outline-dark w-100"
+              disabled={isLoading}
             >
-              {t('current_wallet.button_deposit')}
-            </ExtendedLink>
-          </rb.Col>
-          <rb.Col>
-            {/* Todo: Withdrawing needs to factor in the privacy levels as well.
-          Depending on the mixdepth/account there will be different amounts available. */}
-            <ExtendedLink disabled={isLoading} to={routes.send} className="btn btn-outline-dark w-100">
-              {t('current_wallet.button_withdraw')}
+              <div className="d-flex justify-content-center align-items-center">
+                <Sprite symbol="receive" width="24" height="24" />
+                <div className="ps-1">{t('current_wallet.button_deposit')}</div>
+              </div>
             </ExtendedLink>
           </rb.Col>
         </rb.Row>
diff --git a/src/components/CurrentWalletMagic.module.css b/src/components/CurrentWalletMagic.module.css
index 99289bc..c8d011a 100644
--- a/src/components/CurrentWalletMagic.module.css
+++ b/src/components/CurrentWalletMagic.module.css
@@ -1,9 +1,9 @@
-.wallet-header-title {
+.wallet-header-title-placeholder {
   width: 5rem;
   margin-bottom: 0.5rem;
 }
 
-.wallet-header-subtitle {
+.wallet-header-subtitle-placeholder {
   width: 12rem;
   height: 1.8rem;
   margin-bottom: 0.45rem;
diff --git a/src/components/Navbar.jsx b/src/components/Navbar.jsx
index 4bc05ac..3841b93 100644
--- a/src/components/Navbar.jsx
+++ b/src/components/Navbar.jsx
@@ -26,7 +26,7 @@ const WalletPreview = ({ wallet, walletInfo, unit, showBalance }) => {
             />
           </div>
         ) : (
-          <Balance value="0.00000000" unit="BTC" showBalance={false} loading={true} />
+          <Balance loading={true} />
         )}
       </div>
     </div>
diff --git a/src/components/Receive.jsx b/src/components/Receive.jsx
index 1bc218a..67fae16 100644
--- a/src/components/Receive.jsx
+++ b/src/components/Receive.jsx
@@ -36,12 +36,15 @@ export default function Receive() {
 
     setAlert(null)
     setIsLoading(true)
+
     Api.getAddressNew({ walletName, mixdepth: account, token, signal: abortCtrl.signal })
       .then((res) => (res.ok ? res.json() : Api.Helper.throwError(res, t('receive.error_loading_address_failed'))))
       .then((data) => setAddress(data.address))
       .catch((err) => {
         !abortCtrl.signal.aborted && setAlert({ variant: 'danger', message: err.message })
       })
+      // show the loader a little longer to avoid flickering
+      .then((_) => new Promise((r) => setTimeout(r, 200)))
       .finally(() => !abortCtrl.signal.aborted && setIsLoading(false))
 
     return () => abortCtrl.abort()
@@ -106,8 +109,8 @@ export default function Receive() {
           <div className={styles['qr-container']}>
             {!isLoading && address && <BitcoinQR address={address} sats={amount} />}
             {(isLoading || !address) && (
-              <rb.Placeholder as="p" animation="wave" className={styles['receive-placeholder-qr-container']}>
-                <rb.Placeholder xs={12} className={styles['receive-placeholder-qr']} />
+              <rb.Placeholder as="div" animation="wave" className={styles['receive-placeholder-qr-container']}>
+                <rb.Placeholder className={styles['receive-placeholder-qr']} />
               </rb.Placeholder>
             )}
           </div>
@@ -115,7 +118,7 @@ export default function Receive() {
             {address && <rb.Card.Text className="text-center slashed-zeroes">{address}</rb.Card.Text>}
             {!address && (
               <rb.Placeholder as="p" animation="wave" className={styles['receive-placeholder-container']}>
-                <rb.Placeholder xs={8} className={styles['receive-placeholder']} />
+                <rb.Placeholder xs={12} sm={10} md={8} className={styles['receive-placeholder']} />
               </rb.Placeholder>
             )}
             <div className="d-flex justify-content-center" style={{ gap: '1rem' }}>
@@ -133,7 +136,7 @@ export default function Receive() {
                     }
                   )
                 }}
-                disabled={!address}
+                disabled={!address || isLoading}
               >
                 {showAddressCopiedConfirmation ? (
                   <>
@@ -161,17 +164,14 @@ export default function Receive() {
         </rb.Card>
       </div>
       <div className={styles['settings-container']}>
-        {!isLoading && (
-          <rb.Button
-            variant={`${settings.theme}`}
-            className="ps-0 border-0 d-flex align-items-center"
-            onClick={() => setShowSettings(!showSettings)}
-            disabled={isLoading}
-          >
-            {t('receive.button_settings')}
-            <Sprite symbol={`caret-${showSettings ? 'up' : 'down'}`} className="ms-1" width="20" height="20" />
-          </rb.Button>
-        )}
+        <rb.Button
+          variant={`${settings.theme}`}
+          className="ps-0 border-0 d-flex align-items-center"
+          onClick={() => setShowSettings(!showSettings)}
+        >
+          {t('receive.button_settings')}
+          <Sprite symbol={`caret-${showSettings ? 'up' : 'down'}`} className="ms-1" width="20" height="20" />
+        </rb.Button>
       </div>
       <rb.Form onSubmit={onSubmit} validated={validated} noValidate>
         {showSettings && (
@@ -183,7 +183,7 @@ export default function Receive() {
                   defaultValue={account}
                   onChange={(e) => setAccount(parseInt(e.target.value, 10))}
                   required
-                  disabled={accounts.length === 0}
+                  disabled={isLoading || accounts.length === 0}
                 >
                   {accounts.map((val) => (
                     <option key={val} value={val}>
@@ -203,6 +203,7 @@ export default function Receive() {
                 value={amount}
                 min={0}
                 onChange={(e) => setAmount(e.target.value)}
+                disabled={isLoading}
               />
               <rb.Form.Control.Feedback type="invalid">{t('receive.feedback_invalid_amount')}</rb.Form.Control.Feedback>
             </rb.Form.Group>
@@ -211,7 +212,7 @@ export default function Receive() {
         <hr />
         <rb.Button variant="outline-dark" type="submit" disabled={isLoading} className="mt-2" style={{ width: '100%' }}>
           {isLoading ? (
-            <div>
+            <div className="d-flex justify-content-center align-items-center">
               <rb.Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" className="me-2" />
               {t('receive.text_getting_address')}
             </div>
diff --git a/src/components/Receive.module.css b/src/components/Receive.module.css
index ee7e033..8492515 100644
--- a/src/components/Receive.module.css
+++ b/src/components/Receive.module.css
@@ -1,6 +1,5 @@
 .settings-container {
   margin-top: 1.5rem;
-  height: 2.25rem;
 }
 
 .receive-placeholder-container {
@@ -20,13 +19,17 @@
 }
 
 .receive-placeholder-qr-container {
-  width: 13.5rem;
-  margin-bottom: 0;
+  width: 16.25rem;
 }
 
 .receive-placeholder-qr {
-  width: 13.5rem;
+  width: 13.75rem;
   height: 13.75rem;
-  margin-top: 1.25rem;
-  margin-bottom: 1.25rem;
+  margin: 1.25rem;
+}
+
+:root[data-theme='dark'] .receive-placeholder-qr {
+  width: 16.25rem;
+  height: 16.25rem;
+  margin: 0;
 }
diff --git a/src/components/Send.css b/src/components/Send.css
index 67a72f3..eca7882 100644
--- a/src/components/Send.css
+++ b/src/components/Send.css
@@ -125,8 +125,5 @@ input::-webkit-inner-spin-button {
 
 .send .input-loader {
   height: 3.5rem;
-}
-
-.form-group-without-coinjoin-option {
-  margin-bottom: 3.25rem;
+  border-radius: 0.25rem;
 }
diff --git a/src/components/Send.jsx b/src/components/Send.jsx
index 22652eb..794ccc8 100644
--- a/src/components/Send.jsx
+++ b/src/components/Send.jsx
@@ -456,7 +456,7 @@ export default function Send() {
                   <a
                     href="https://github.com/JoinMarket-Org/joinmarket-clientserver#wallet-features"
                     target="_blank"
-                    rel="noreferrer"
+                    rel="noopener noreferrer"
                   >
                     frozen
                   </a>
@@ -464,7 +464,7 @@ export default function Send() {
                   <a
                     href="https://github.com/JoinMarket-Org/joinmarket-clientserver/blob/master/docs/fidelity-bonds.md"
                     target="_blank"
-                    rel="noreferrer"
+                    rel="noopener noreferrer"
                   >
                     time-locked
                   </a>
@@ -475,7 +475,7 @@ export default function Send() {
                   <a
                     href="https://github.com/JoinMarket-Org/JoinMarket-Docs/blob/master/High-level-design.md#joinmarket-transaction-types"
                     target="_blank"
-                    rel="noreferrer"
+                    rel="noopener noreferrer"
                   >
                     JoinMarket documentation
                   </a>
@@ -514,31 +514,12 @@ export default function Send() {
         )}
 
         <rb.Form onSubmit={onSubmit} noValidate id="send-form">
-          <rb.Form.Group className="mb-4" controlId="destination">
-            <rb.Form.Label>{t('send.label_recipient')}</rb.Form.Label>
-            {isLoading ? (
-              <rb.Placeholder as="p" animation="wave">
-                <rb.Placeholder xs={12} className="input-loader" />
-              </rb.Placeholder>
-            ) : (
-              <rb.Form.Control
-                name="destination"
-                placeholder={t('send.placeholder_recipient')}
-                className="slashed-zeroes"
-                value={destination || ''}
-                required
-                onChange={(e) => setDestination(e.target.value)}
-                isInvalid={destination !== null && !isValidAddress(destination)}
-              />
-            )}
-            <rb.Form.Control.Feedback type="invalid">{t('send.feedback_invalid_recipient')}</rb.Form.Control.Feedback>
-          </rb.Form.Group>
           <rb.Form.Group className="mb-4 flex-grow-1" controlId="account">
             <rb.Form.Label>
               {settings.useAdvancedWalletMode ? t('send.label_account_dev_mode') : t('send.label_account')}
             </rb.Form.Label>
             {isLoading ? (
-              <rb.Placeholder as="p" animation="wave">
+              <rb.Placeholder as="div" animation="wave">
                 <rb.Placeholder xs={12} className="input-loader" />
               </rb.Placeholder>
             ) : (
@@ -563,14 +544,11 @@ export default function Send() {
               </rb.Form.Select>
             )}
           </rb.Form.Group>
-          <rb.Form.Group
-            className={isCoinjoinOptionEnabled ? 'mb-4' : 'form-group-without-coinjoin-option'}
-            controlId="amount"
-          >
+          <rb.Form.Group className={isSweep ? 'mb-0' : 'mb-4'} controlId="amount">
             <rb.Form.Label form="send-form">{t('send.label_amount')}</rb.Form.Label>
             <div className="position-relative">
               {isLoading ? (
-                <rb.Placeholder as="p" animation="wave">
+                <rb.Placeholder as="div" animation="wave">
                   <rb.Placeholder xs={12} className="input-loader" />
                 </rb.Placeholder>
               ) : (
@@ -609,9 +587,32 @@ export default function Send() {
             </rb.Form.Control.Feedback>
             {isSweep && frozenOrLockedWarning()}
           </rb.Form.Group>
+          <rb.Form.Group className="mb-4" controlId="destination">
+            <rb.Form.Label>{t('send.label_recipient')}</rb.Form.Label>
+            {isLoading ? (
+              <rb.Placeholder as="div" animation="wave">
+                <rb.Placeholder xs={12} className="input-loader" />
+              </rb.Placeholder>
+            ) : (
+              <rb.Form.Control
+                name="destination"
+                placeholder={t('send.placeholder_recipient')}
+                className="slashed-zeroes"
+                value={destination || ''}
+                required
+                onChange={(e) => setDestination(e.target.value)}
+                isInvalid={destination !== null && !isValidAddress(destination)}
+              />
+            )}
+            <rb.Form.Control.Feedback type="invalid">{t('send.feedback_invalid_recipient')}</rb.Form.Control.Feedback>
+          </rb.Form.Group>
           {isCoinjoinOptionEnabled && (
             <rb.Form.Group controlId="isCoinjoin" className={`${isCoinjoin ? 'mb-3' : ''}`}>
-              <ToggleSwitch label={t('send.toggle_coinjoin')} onToggle={(isToggled) => setIsCoinjoin(isToggled)} />
+              <ToggleSwitch
+                label={t('send.toggle_coinjoin')}
+                initialValue={isCoinjoin}
+                onToggle={(isToggled) => setIsCoinjoin(isToggled)}
+              />
             </rb.Form.Group>
           )}
         </rb.Form>
@@ -624,7 +625,7 @@ export default function Send() {
         )}
         <rb.Button variant="dark" type="submit" disabled={isSending || !formIsValid} className="mt-4" form="send-form">
           {isSending ? (
-            <div>
+            <div className="d-flex justify-content-center align-items-center">
               <rb.Spinner as="span" animation="border" size="sm" role="status" aria-hidden="true" className="me-2" />
               {t('send.text_sending')}
             </div>


Test patch:
diff --git a/src/components/Balance.test.jsx b/src/components/Balance.test.jsx
index 287ede3..551c499 100644
--- a/src/components/Balance.test.jsx
+++ b/src/components/Balance.test.jsx
@@ -5,6 +5,11 @@ import { BTC, SATS } from '../utils'
 import Balance from './Balance'
 
 describe('<Balance />', () => {
+  it('should render placeholder while loading', () => {
+    render(<Balance loading={true} />)
+    expect(screen.getByTestId('balance-component-placeholder')).toBeInTheDocument()
+  })
+
   it('should render BTC using satscomma formatting', () => {
     render(<Balance valueString={'123.456'} convertToUnit={BTC} showBalance={true} />)
     expect(screen.getByText(`123.45 600 000`)).toBeInTheDocument()