diff --git a/public/locales/en.json b/public/locales/en.json index c4d1284..f3ca78b 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -232,26 +232,7 @@ "walletOverviewAddWallets": "Add wallets...", "transactionsCardTitle": "Transactions", - "transactionItemID": "Transaction ID: {{id}}", - "transactionItemFrom": "<0>From: <1 />", - "transactionItemTo": "<0>To: <1 />", - "transactionItemName": "<0>Name: <1 />", - "transactionItemARecord": "<0>A record: <1 />", - "transactionItemARecordRemoved": "(removed)", - "transactionTypes": { - "transferred": "Transferred", - "sent": "Sent", - "received": "Received", - "mined": "Mined", - "name_a_record": "Updated name", - "name_transferred": "Moved name", - "name_sent": "Sent name", - "name_received": "Received name", - "name_purchased": "Purchased name", - "unknown": "Unknown" - }, "transactionsError": "There was an error fetching your transactions. See the console for details.", - "transactionsSeeMore": "See all {{count, number}}...", "blockValueCardTitle": "Block Value", "blockValueBaseValue": "Base value (<1>)", @@ -353,5 +334,27 @@ "resultNotFound": "That address does not exist.", "resultUnknownTitle": "Unknown error", "resultUnknown": "See console for details." + }, + + "transactionSummary": { + "itemID": "Transaction ID: {{id}}", + "itemFrom": "<0>From: <1 />", + "itemTo": "<0>To: <1 />", + "itemName": "<0>Name: <1 />", + "itemARecord": "<0>A record: <1 />", + "itemARecordRemoved": "(removed)", + "types": { + "transferred": "Transferred", + "sent": "Sent", + "received": "Received", + "mined": "Mined", + "name_a_record": "Updated name", + "name_transferred": "Moved name", + "name_sent": "Sent name", + "name_received": "Received name", + "name_purchased": "Purchased name", + "unknown": "Unknown" + }, + "seeMore": "See all {{count, number}}..." } } diff --git a/src/components/transactions/TransactionItem.tsx b/src/components/transactions/TransactionItem.tsx new file mode 100644 index 0000000..25f1518 --- /dev/null +++ b/src/components/transactions/TransactionItem.tsx @@ -0,0 +1,165 @@ +// Copyright (c) 2020-2021 Drew Lemmy +// This file is part of KristWeb 2 under GPL-3.0. +// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt +import React from "react"; +import { Row, Col, Tooltip, Grid } from "antd"; + +import TimeAgo from "react-timeago"; + +import { useTranslation, Trans } from "react-i18next"; +import { Link } from "react-router-dom"; + +import { KristTransaction } from "../../krist/api/types"; +import { Wallet } from "../../krist/wallets/Wallet"; +import { KristValue } from "../KristValue"; +import { KristNameLink } from "../KristNameLink"; +import { ContextualAddress } from "../ContextualAddress"; + +type InternalTxType = "transferred" | "sent" | "received" | "mined" | + "name_a_record" | "name_transferred" | "name_sent" | "name_received" | + "name_purchased" | "unknown"; +const TYPES_SHOW_VALUE = ["transferred", "sent", "received", "mined", "name_purchased"]; + +const MAX_A_LENGTH = 24; + +interface Props { + transaction: KristTransaction; + + /** [address]: Wallet */ + wallets: Record; +} + +function getTxType(tx: KristTransaction, from: Wallet | undefined, to: Wallet | undefined): InternalTxType { + switch (tx.type) { + case "transfer": + if (from && to) return "transferred"; + if (from) return "sent"; + if (to) return "received"; + return "transferred"; + + case "name_transfer": + if (from && to) return "name_transferred"; + if (from) return "name_sent"; + if (to) return "name_received"; + return "name_transferred"; + + case "name_a_record": return "name_a_record"; + case "name_purchase": return "name_purchased"; + + case "mined": return "mined"; + + default: return "unknown"; + } +} + +export function TransactionARecord({ metadata }: { metadata: string | undefined | null }): JSX.Element { + const { t } = useTranslation(); + + return metadata + ? + + {metadata.length > MAX_A_LENGTH + ? <>{metadata.substring(0, MAX_A_LENGTH)}… + : metadata} + + + : ( + + {t("transactionSummary.itemARecordRemoved")} + + ); +} + +export function TransactionItem({ transaction: tx, wallets }: Props): JSX.Element { + const { t } = useTranslation(); + const bps = Grid.useBreakpoint(); + + // Whether or not the from/to addresses are a wallet we own + // TODO: Address book here too + const fromWallet = tx.from ? wallets[tx.from] : undefined; + const toWallet = tx.to ? wallets[tx.to] : undefined; + + const type = getTxType(tx, fromWallet, toWallet); + + const txTime = new Date(tx.time); + const isNew = (new Date().getTime() - txTime.getTime()) < 360000; + + const txLink = "/network/transactions/" + encodeURIComponent(tx.id); + + const hideNameAddress = !bps.xl; + + return + + {/* Transaction type and link to transaction */} + + + {t("transactionSummary.types." + type)} + + + + {/* Transaction time */} + + + + + + + {/* Transaction name */} + {(type === "name_a_record" || type === "name_purchased") && ( + + Name: + + + )} + + {/* Transaction A record */} + {type === "name_a_record" && ( + + A record: + + + )} + + {/* Transaction to */} + {type !== "name_a_record" && ( + + To: + {type === "name_purchased" + ? + : } + + )} + + {/* Transaction from */} + {type !== "name_a_record" && type !== "name_purchased" && type !== "mined" && ( + + From: + + + )} + + + + {TYPES_SHOW_VALUE.includes(type) + ? ( + // Transaction value + + ) + : tx.type === "name_transfer" && ( + // Transaction name + + )} + + ; +} diff --git a/src/components/transactions/TransactionSummary.less b/src/components/transactions/TransactionSummary.less new file mode 100644 index 0000000..01206ea --- /dev/null +++ b/src/components/transactions/TransactionSummary.less @@ -0,0 +1,83 @@ +// Copyright (c) 2020-2021 Drew Lemmy +// This file is part of KristWeb 2 under GPL-3.0. +// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt +@import (reference) "../../App.less"; + +.transaction-summary-item { + flex-flow: nowrap; + + .transaction-left { + display: flex; + flex-direction: column; + justify-content: center; + + .transaction-type { + a { + font-weight: bold; + color: @text-color-secondary; + } + + &-transferred a, &-name_transferred a { color: @kw-primary; } + &-sent a, &-name_sent a, &-name_purchased a { color: @kw-orange; } + &-received a, &-mined a, &-name_received a { color: @kw-green; } + &-name_a_record a { color: @kw-purple; } + + @media (max-width: @screen-xl) { + font-size: 90%; + } + } + + .transaction-time { + color: @text-color-secondary; + font-size: 90%; + + @media (max-width: @screen-xl) { + font-size: 85%; + } + } + } + + .transaction-middle { + flex: 1; + + display: flex; + flex-direction: column; + justify-content: center; + + overflow: hidden; + + .transaction-field { + font-weight: bold; + white-space: nowrap; + color: @text-color-secondary; + } + + .transaction-a-record-value { + font-family: monospace; + font-size: 90%; + color: @text-color-secondary; + } + + .transaction-a-record-removed { + font-style: italic; + font-size: 90%; + color: @text-color-secondary; + + white-space: nowrap; + text-overflow: ellipsis; + } + } + + .transaction-right { + flex: 0; + font-size: @font-size-lg; + + display: flex; + justify-content: center; + align-items: center; + + .transaction-name { + font-weight: bold; + } + } +} diff --git a/src/components/transactions/TransactionSummary.tsx b/src/components/transactions/TransactionSummary.tsx new file mode 100644 index 0000000..ccb9310 --- /dev/null +++ b/src/components/transactions/TransactionSummary.tsx @@ -0,0 +1,44 @@ +// Copyright (c) 2020-2021 Drew Lemmy +// This file is part of KristWeb 2 under GPL-3.0. +// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt +import React from "react"; +import { Row } from "antd"; + +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; + +import { useWallets } from "../../krist/wallets/Wallet"; + +import { KristTransaction } from "../../krist/api/types"; +import { TransactionItem } from "./TransactionItem"; + +import "./TransactionSummary.less"; + +interface Props { + transactions?: KristTransaction[]; + + seeMoreCount?: number; + seeMoreKey?: string; + seeMoreLink?: string; +} + +export function TransactionSummary({ transactions, seeMoreCount, seeMoreKey, seeMoreLink }: Props): JSX.Element { + const { t } = useTranslation(); + const { walletAddressMap } = useWallets(); + + return <> + {transactions && transactions.map(t => ( + + ))} + + {seeMoreCount !== undefined && + + {t(seeMoreKey || "transactionSummary.seeMore", { count: seeMoreCount })} + + } + ; +} diff --git a/src/components/wallets/SelectWalletCategory.tsx b/src/components/wallets/SelectWalletCategory.tsx index fa82d1f..46561c9 100644 --- a/src/components/wallets/SelectWalletCategory.tsx +++ b/src/components/wallets/SelectWalletCategory.tsx @@ -5,11 +5,10 @@ import { Select, Input, Button, Typography, Divider } from "antd"; import { PlusOutlined } from "@ant-design/icons"; -import { useSelector, shallowEqual } from "react-redux"; -import { RootState } from "../../store"; import { useTranslation } from "react-i18next"; import { localeSort } from "../../utils"; +import { useWallets } from "../../krist/wallets/Wallet"; const { Text } = Typography; @@ -19,7 +18,7 @@ export function SelectWalletCategory({ onNewCategory }: Props): JSX.Element { // Required to fetch existing categories - const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); + const { wallets } = useWallets(); const existingCategories = [...new Set(Object.values(wallets) .filter(w => w.category !== undefined && w.category !== "") .map(w => w.category) as string[])]; diff --git a/src/components/wallets/SyncWallets.tsx b/src/components/wallets/SyncWallets.tsx index e5574a3..07f8e69 100644 --- a/src/components/wallets/SyncWallets.tsx +++ b/src/components/wallets/SyncWallets.tsx @@ -1,22 +1,17 @@ // Copyright (c) 2020-2021 Drew Lemmy // This file is part of KristWeb 2 under GPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt -import { useSelector } from "react-redux"; -import { RootState } from "../../store"; - import { syncWallets } from "../../krist/wallets/Wallet"; import { useMountEffect } from "../../utils"; /** Sync the wallets with the Krist node on startup. */ export function SyncWallets(): JSX.Element | null { - const { wallets } = useSelector((s: RootState) => s.wallets); - // 'wallets' is not a dependency here, we don't want to re-fetch on every // wallet update, just the initial startup. We'll leave fetching wallets when // the syncNode changes to the WebsocketService. useMountEffect(() => { // TODO: show errors to the user? - syncWallets(wallets).catch(console.error); + syncWallets().catch(console.error); }); return null; diff --git a/src/components/ws/SyncMOTD.tsx b/src/components/ws/SyncMOTD.tsx index d8bcb73..ded78bd 100644 --- a/src/components/ws/SyncMOTD.tsx +++ b/src/components/ws/SyncMOTD.tsx @@ -3,7 +3,7 @@ // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt import { useEffect } from "react"; -import { useSelector, shallowEqual } from "react-redux"; +import { useSelector } from "react-redux"; import { RootState } from "../../store"; import * as nodeActions from "../../store/actions/NodeActions"; @@ -12,7 +12,7 @@ import * as api from "../../krist/api"; import { KristMOTD } from "../../krist/api/types"; -import { recalculateWallets } from "../../krist/wallets/Wallet"; +import { recalculateWallets, useWallets } from "../../krist/wallets/Wallet"; import Debug from "debug"; const debug = Debug("kristweb:sync-motd"); @@ -34,7 +34,7 @@ // All these are used to determine if we need to recalculate the addresses const addressPrefix = useSelector((s: RootState) => s.node.currency.address_prefix); const masterPassword = useSelector((s: RootState) => s.walletManager.masterPassword); - const wallets = useSelector((s: RootState) => s.wallets.wallets, shallowEqual); + const { wallets } = useWallets(); // Update the MOTD when the sync node changes, and on startup useEffect(() => { diff --git a/src/components/ws/WebsocketService.tsx b/src/components/ws/WebsocketService.tsx index 9405e6e..34d0b4a 100644 --- a/src/components/ws/WebsocketService.tsx +++ b/src/components/ws/WebsocketService.tsx @@ -3,16 +3,14 @@ // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt import { useState, useEffect } from "react"; -import { useSelector, shallowEqual } from "react-redux"; import { store } from "../../App"; -import { RootState } from "../../store"; import { WalletMap } from "../../store/reducers/WalletsReducer"; import * as wsActions from "../../store/actions/WebsocketActions"; import * as nodeActions from "../../store/actions/NodeActions"; import * as api from "../../krist/api"; import { KristAddress, KristBlock, KristTransaction, WSConnectionState, WSIncomingMessage, WSSubscriptionLevel } from "../../krist/api/types"; -import { findWalletByAddress, syncWallet, syncWalletUpdate } from "../../krist/wallets/Wallet"; +import { useWallets, findWalletByAddress, syncWallet, syncWalletUpdate } from "../../krist/wallets/Wallet"; import WebSocketAsPromised from "websocket-as-promised"; import { throttle } from "lodash-es"; @@ -226,7 +224,7 @@ } export function WebsocketService(): JSX.Element | null { - const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); + const { wallets } = useWallets(); const syncNode = api.useSyncNode(); const [connection, setConnection] = useState(); diff --git a/src/global/AppServices.tsx b/src/global/AppServices.tsx index 7b52a52..66105ec 100644 --- a/src/global/AppServices.tsx +++ b/src/global/AppServices.tsx @@ -1,6 +1,8 @@ // Copyright (c) 2020-2021 Drew Lemmy // This file is part of KristWeb 2 under GPL-3.0. // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt +import React from "react"; + import { SyncWallets } from "../components/wallets/SyncWallets"; import { ForcedAuth } from "../components/auth/ForcedAuth"; import { WebsocketService } from "../components/ws/WebsocketService"; diff --git a/src/krist/wallets/Wallet.ts b/src/krist/wallets/Wallet.ts index 99519ff..7adecc0 100644 --- a/src/krist/wallets/Wallet.ts +++ b/src/krist/wallets/Wallet.ts @@ -14,6 +14,9 @@ import * as actions from "../../store/actions/WalletsActions"; import { WalletMap } from "../../store/reducers/WalletsReducer"; +import { useSelector, shallowEqual } from "react-redux"; +import { RootState } from "../../store"; + import { Mutex } from "async-mutex"; import Debug from "debug"; @@ -164,7 +167,9 @@ /** Sync the data for all the wallets from the sync node, save it to local * storage, and dispatch the changes to the Redux store. */ -export async function syncWallets(wallets: WalletMap): Promise { +export async function syncWallets(): Promise { + const { wallets } = store.getState().wallets; + const syncTime = new Date(); // Fetch all the data from the sync node (e.g. balances) @@ -376,3 +381,12 @@ localStorage.setItem("lastAddressPrefix", addressPrefix); }); } + +/** Hook that fetches the wallets from the Redux store. */ +export function useWallets(): { wallets: WalletMap; walletAddressMap: Record } { + const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); + const walletAddressMap = Object.values(wallets) + .reduce((o, wallet) => ({ ...o, [wallet.address]: wallet }), {}); + + return { wallets, walletAddressMap }; +} diff --git a/src/layout/sidebar/SidebarTotalBalance.tsx b/src/layout/sidebar/SidebarTotalBalance.tsx index f16df59..11499aa 100644 --- a/src/layout/sidebar/SidebarTotalBalance.tsx +++ b/src/layout/sidebar/SidebarTotalBalance.tsx @@ -4,15 +4,13 @@ import React from "react"; import { useTranslation } from "react-i18next"; -import { useSelector, shallowEqual } from "react-redux"; -import { RootState } from "../../store"; - +import { useWallets } from "../../krist/wallets/Wallet"; import { KristValue } from "../../components/KristValue"; export function SidebarTotalBalance(): JSX.Element { const { t } = useTranslation(); - const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); + const { wallets } = useWallets(); const balance = Object.values(wallets) .filter(w => w.balance !== undefined) .reduce((acc, w) => acc + w.balance!, 0); diff --git a/src/pages/dashboard/DashboardPage.less b/src/pages/dashboard/DashboardPage.less index 3922a74..0d8796f 100644 --- a/src/pages/dashboard/DashboardPage.less +++ b/src/pages/dashboard/DashboardPage.less @@ -41,87 +41,6 @@ } } - .dashboard-card-transactions { - .dashboard-transaction-item { - flex-flow: nowrap; - - .transaction-left { - display: flex; - flex-direction: column; - justify-content: center; - - .transaction-type { - a { - font-weight: bold; - color: @text-color-secondary; - } - - &-transferred a, &-name_transferred a { color: @kw-primary; } - &-sent a, &-name_sent a, &-name_purchased a { color: @kw-orange; } - &-received a, &-mined a, &-name_received a { color: @kw-green; } - &-name_a_record a { color: @kw-purple; } - - @media (max-width: @screen-xl) { - font-size: 90%; - } - } - - .transaction-time { - color: @text-color-secondary; - font-size: 90%; - - @media (max-width: @screen-xl) { - font-size: 85%; - } - } - } - - .transaction-middle { - flex: 1; - - display: flex; - flex-direction: column; - justify-content: center; - - overflow: hidden; - - .transaction-field { - font-weight: bold; - white-space: nowrap; - color: @text-color-secondary; - } - - .transaction-a-record-value { - font-family: monospace; - font-size: 90%; - color: @text-color-secondary; - } - - .transaction-a-record-removed { - font-style: italic; - font-size: 90%; - color: @text-color-secondary; - - white-space: nowrap; - text-overflow: ellipsis; - } - } - - .transaction-right { - flex: 0; - font-size: @font-size-lg; - - display: flex; - justify-content: center; - align-items: center; - - .transaction-name { - font-weight: bold; - } - } - } - } - .dashboard-card-block-value { .dashboard-block-value-main.krist-value { font-size: @heading-4-size; @@ -191,37 +110,4 @@ } } } - - .dashboard-list-item { - padding: (@padding-sm / 2) @padding-md; - margin-bottom: @margin-xs; - - background: @kw-dark; - border-radius: @kw-big-card-border-radius; - - box-sizing: border-box; - min-height: 60px; - justify-content: center; - - line-height: 1.5; - - &:last-child { - margin-bottom: 0; - } - } - - .dashboard-more a { - display: block; - width: 100%; - - margin-bottom: -@padding-sm; - - color: @text-color-secondary; - font-size: 90%; - text-align: center; - - &:hover { - color: lighten(@text-color-secondary, 10%); - } - } } diff --git a/src/pages/dashboard/TransactionItem.tsx b/src/pages/dashboard/TransactionItem.tsx deleted file mode 100644 index 2275062..0000000 --- a/src/pages/dashboard/TransactionItem.tsx +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright (c) 2020-2021 Drew Lemmy -// This file is part of KristWeb 2 under GPL-3.0. -// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt -import React from "react"; -import { Row, Col, Tooltip, Grid } from "antd"; - -import TimeAgo from "react-timeago"; - -import { useTranslation, Trans } from "react-i18next"; -import { Link } from "react-router-dom"; - -import { KristTransaction } from "../../krist/api/types"; -import { Wallet } from "../../krist/wallets/Wallet"; -import { KristValue } from "../../components/KristValue"; -import { KristNameLink } from "../../components/KristNameLink"; -import { ContextualAddress } from "../../components/ContextualAddress"; - -type InternalTxType = "transferred" | "sent" | "received" | "mined" | - "name_a_record" | "name_transferred" | "name_sent" | "name_received" | - "name_purchased" | "unknown"; -const TYPES_SHOW_VALUE = ["transferred", "sent", "received", "mined", "name_purchased"]; - -const MAX_A_LENGTH = 24; - -interface Props { - transaction: KristTransaction; - - /** [address]: Wallet */ - wallets: Record; -} - -function getTxType(tx: KristTransaction, from: Wallet | undefined, to: Wallet | undefined): InternalTxType { - switch (tx.type) { - case "transfer": - if (from && to) return "transferred"; - if (from) return "sent"; - if (to) return "received"; - return "transferred"; - - case "name_transfer": - if (from && to) return "name_transferred"; - if (from) return "name_sent"; - if (to) return "name_received"; - return "name_transferred"; - - case "name_a_record": return "name_a_record"; - case "name_purchase": return "name_purchased"; - - case "mined": return "mined"; - - default: return "unknown"; - } -} - -export function TransactionARecord({ metadata }: { metadata: string | undefined | null }): JSX.Element { - const { t } = useTranslation(); - - return metadata - ? - - {metadata.length > MAX_A_LENGTH - ? <>{metadata.substring(0, MAX_A_LENGTH)}… - : metadata} - - - : ( - - {t("dashboard.transactionItemARecordRemoved")} - - ); -} - -export function TransactionItem({ transaction: tx, wallets }: Props): JSX.Element { - const { t } = useTranslation(); - const bps = Grid.useBreakpoint(); - - const fromWallet = tx.from ? wallets[tx.from] : undefined; - const toWallet = tx.to ? wallets[tx.to] : undefined; - - const type = getTxType(tx, fromWallet, toWallet); - - const txTime = new Date(tx.time); - const isNew = (new Date().getTime() - txTime.getTime()) < 360000; - - const txLink = "/network/transactions/" + encodeURIComponent(tx.id); - - const hideNameAddress = !bps.xl; - - return - - {/* Transaction type and link to transaction */} - - - {t("dashboard.transactionTypes." + type)} - - - - {/* Transaction time */} - - - - - - - {/* Transaction name */} - {(type === "name_a_record" || type === "name_purchased") && ( - - Name: - - - )} - - {/* Transaction A record */} - {type === "name_a_record" && ( - - A record: - - - )} - - {/* Transaction to */} - {type !== "name_a_record" && ( - - To: - {type === "name_purchased" - ? - : } - - )} - - {/* Transaction from */} - {type !== "name_a_record" && type !== "name_purchased" && type !== "mined" && ( - - From: - - - )} - - - - {TYPES_SHOW_VALUE.includes(type) - ? ( - // Transaction value - - ) - : tx.type === "name_transfer" && ( - // Transaction name - - )} - - ; -} diff --git a/src/pages/dashboard/TransactionsCard.tsx b/src/pages/dashboard/TransactionsCard.tsx index b6cc518..c6d8b0f 100644 --- a/src/pages/dashboard/TransactionsCard.tsx +++ b/src/pages/dashboard/TransactionsCard.tsx @@ -3,18 +3,17 @@ // Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt import React, { useState, useEffect, useMemo } from "react"; import classNames from "classnames"; -import { Card, Skeleton, Empty, Row } from "antd"; +import { Card, Skeleton, Empty } from "antd"; -import { useSelector, shallowEqual } from "react-redux"; -import { RootState } from "../../store"; import { useTranslation } from "react-i18next"; -import { Link } from "react-router-dom"; -import { TransactionItem } from "./TransactionItem"; -import { WalletMap } from "../../store/reducers/WalletsReducer"; -import { useSyncNode } from "../../krist/api"; +import { TransactionSummary } from "../../components/transactions/TransactionSummary"; import { lookupTransactions, LookupTransactionsResponse } from "../../krist/api/lookup"; +import { useSyncNode } from "../../krist/api"; +import { useWallets } from "../../krist/wallets/Wallet"; +import { WalletMap } from "../../store/reducers/WalletsReducer"; + import { SmallResult } from "../../components/SmallResult"; import { trailingThrottleState } from "../../utils/promiseThrottle"; @@ -34,7 +33,7 @@ export function TransactionsCard(): JSX.Element { const syncNode = useSyncNode(); - const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); + const { wallets } = useWallets(); const { t } = useTranslation(); const [res, setRes] = useState(); @@ -48,27 +47,6 @@ fetchTxs(wallets); }, [syncNode, wallets, fetchTxs]); - const walletAddressMap = Object.values(wallets) - .reduce((o, wallet) => ({ ...o, [wallet.address]: wallet }), {}); - - function cardContents(): JSX.Element { - return <> - {res && res.transactions.map(t => ( - - ))} - - - - {t("dashboard.transactionsSeeMore", { count: res?.total || 0 })} - - - ; - } - const isEmpty = !loading && (error || !res || res.count === 0); const classes = classNames("kw-card", "dashboard-card-transactions", { "empty": isEmpty @@ -79,7 +57,13 @@ {error ? : (res && res.count > 0 - ? cardContents() + ? ( + + ) : )} diff --git a/src/pages/dashboard/WalletItem.tsx b/src/pages/dashboard/WalletItem.tsx index e15cce3..7fb8ca3 100644 --- a/src/pages/dashboard/WalletItem.tsx +++ b/src/pages/dashboard/WalletItem.tsx @@ -9,7 +9,7 @@ import { KristValue } from "../../components/KristValue"; export function WalletItem({ wallet }: { wallet: Wallet }): JSX.Element { - return + return {wallet.label && {wallet.label}} {wallet.address} diff --git a/src/pages/dashboard/WalletOverviewCard.tsx b/src/pages/dashboard/WalletOverviewCard.tsx index 0c92327..e009889 100644 --- a/src/pages/dashboard/WalletOverviewCard.tsx +++ b/src/pages/dashboard/WalletOverviewCard.tsx @@ -4,12 +4,10 @@ import React from "react"; import { Card, Row, Col, Button } from "antd"; -import { useSelector, shallowEqual } from "react-redux"; -import { RootState } from "../../store"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; -import { Wallet } from "../../krist/wallets/Wallet"; +import { Wallet, useWallets } from "../../krist/wallets/Wallet"; import { KristValue } from "../../components/KristValue"; import { Statistic } from "../../components/Statistic"; @@ -18,7 +16,7 @@ import { keyedNullSort } from "../../utils"; export function WalletOverviewCard(): JSX.Element { - const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); + const { wallets } = useWallets(); const { t } = useTranslation(); const clonedWallets = [...Object.values(wallets)]; @@ -53,7 +51,7 @@ {top4Wallets.map(w => )} - + {clonedWallets.length > 0 ? t("dashboard.walletOverviewSeeMore", { count: clonedWallets.length }) diff --git a/src/pages/wallets/AddWalletModal.tsx b/src/pages/wallets/AddWalletModal.tsx index b184136..8b668ba 100644 --- a/src/pages/wallets/AddWalletModal.tsx +++ b/src/pages/wallets/AddWalletModal.tsx @@ -5,7 +5,7 @@ import { Modal, Form, Input, Checkbox, Collapse, Button, Tooltip, Typography, Row, Col, message, notification, Grid } from "antd"; import { ReloadOutlined } from "@ant-design/icons"; -import { useSelector, shallowEqual } from "react-redux"; +import { useSelector } from "react-redux"; import { RootState } from "../../store"; import { useTranslation, Trans } from "react-i18next"; @@ -18,7 +18,7 @@ import { WalletFormatName, applyWalletFormat, formatNeedsUsername } from "../../krist/wallets/formats/WalletFormat"; import { SelectWalletFormat } from "../../components/wallets/SelectWalletFormat"; import { makeV2Address } from "../../krist/AddressAlgo"; -import { addWallet, decryptWallet, editWallet, Wallet, ADDRESS_LIST_LIMIT } from "../../krist/wallets/Wallet"; +import { useWallets, addWallet, decryptWallet, editWallet, Wallet, ADDRESS_LIST_LIMIT } from "../../krist/wallets/Wallet"; const { Text } = Typography; const { useBreakpoint } = Grid; @@ -50,9 +50,9 @@ const initialFormat = editing?.format || "kristwallet"; // Required to encrypt new wallets - const { masterPassword } = useSelector((s: RootState) => s.walletManager, shallowEqual); + const masterPassword = useSelector((s: RootState) => s.walletManager.masterPassword); // Required to check for existing wallets - const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); + const { wallets } = useWallets(); const addressPrefix = useSelector((s: RootState) => s.node.currency.address_prefix); const { t } = useTranslation(); diff --git a/src/pages/wallets/WalletsPage.tsx b/src/pages/wallets/WalletsPage.tsx index 49a3ec1..2f4a0d3 100644 --- a/src/pages/wallets/WalletsPage.tsx +++ b/src/pages/wallets/WalletsPage.tsx @@ -5,8 +5,6 @@ import { Button } from "antd"; import { DatabaseOutlined, PlusOutlined } from "@ant-design/icons"; -import { useSelector, shallowEqual } from "react-redux"; -import { RootState } from "../../store"; import { useTranslation } from "react-i18next"; import { PageLayout } from "../../layout/PageLayout"; @@ -14,13 +12,15 @@ import { AddWalletModal } from "./AddWalletModal"; import { WalletsTable } from "./WalletsTable"; +import { useWallets } from "../../krist/wallets/Wallet"; + import "./WalletsPage.less"; /** Extract the subtitle into its own component to avoid re-rendering the * entire page when a wallet is added. */ function WalletsPageSubtitle(): JSX.Element { const { t } = useTranslation(); - const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); + const { wallets } = useWallets(); return <>{t("myWallets.walletCount", { count: Object.keys(wallets).length })}; } diff --git a/src/pages/wallets/WalletsTable.tsx b/src/pages/wallets/WalletsTable.tsx index 7b3344b..dd4bf7d 100644 --- a/src/pages/wallets/WalletsTable.tsx +++ b/src/pages/wallets/WalletsTable.tsx @@ -5,8 +5,6 @@ import { Table, Tooltip, Dropdown, Tag, Menu, Popconfirm } from "antd"; import { EditOutlined, DeleteOutlined } from "@ant-design/icons"; -import { useSelector, shallowEqual } from "react-redux"; -import { RootState } from "../../store"; import { useTranslation } from "react-i18next"; import { KristValue } from "../../components/KristValue"; @@ -14,7 +12,7 @@ import { AuthorisedAction } from "../../components/auth/AuthorisedAction"; import { AddWalletModal } from "./AddWalletModal"; -import { Wallet, deleteWallet } from "../../krist/wallets/Wallet"; +import { Wallet, deleteWallet, useWallets } from "../../krist/wallets/Wallet"; import { keyedNullSort, localeSort } from "../../utils"; @@ -74,8 +72,7 @@ export function WalletsTable(): JSX.Element { const { t } = useTranslation(); - - const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); + const { wallets } = useWallets(); // Required to filter by categories const categories = [...new Set(Object.values(wallets) diff --git a/src/store/init.ts b/src/store/init.ts index 08e93bb..d1a1ad4 100644 --- a/src/store/init.ts +++ b/src/store/init.ts @@ -6,11 +6,13 @@ import { getInitialSettingsState } from "./reducers/SettingsReducer"; import { getInitialNodeState } from "./reducers/NodeReducer"; -import { createStore } from "redux"; +import { createStore, Store } from "redux"; import { devToolsEnhancer } from "redux-devtools-extension"; import rootReducer from "./reducers/RootReducer"; -export const initStore = () => createStore( +import { RootState, RootAction } from "./index"; + +export const initStore = (): Store => createStore( rootReducer, { walletManager: getInitialWalletManagerState(), diff --git a/src/style/card.less b/src/style/card.less index d4f3cef..ece38b9 100644 --- a/src/style/card.less +++ b/src/style/card.less @@ -58,4 +58,37 @@ padding: @margin-md 0; } } + + .card-list-item { + padding: (@padding-sm / 2) @padding-md; + margin-bottom: @margin-xs; + + background: @kw-dark; + border-radius: @kw-big-card-border-radius; + + box-sizing: border-box; + min-height: 60px; + justify-content: center; + + line-height: 1.5; + + &:last-child { + margin-bottom: 0; + } + } + + .card-more a { + display: block; + width: 100%; + + margin-bottom: -@padding-sm; + + color: @text-color-secondary; + font-size: 90%; + text-align: center; + + &:hover { + color: lighten(@text-color-secondary, 10%); + } + } }