mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-08-24 14:15:55 -04:00
feat(gui): Rework transaction history ui (#470)
Co-authored-by: b-enedict <benedict.seuss@gmail.com>
This commit is contained in:
parent
c1c45571f0
commit
1ad4bcadf5
7 changed files with 331 additions and 89 deletions
|
@ -1 +1 @@
|
|||
Subproject commit dbbccecc89e1121762a4ad6b531638ece82aa0c7
|
||||
Subproject commit 5f714f147fd29228698070e6bd80e41ce2f86fb0
|
|
@ -1,4 +1,4 @@
|
|||
import { Tooltip } from "@mui/material";
|
||||
import { Box, SxProps, Tooltip, Typography } from "@mui/material";
|
||||
import { useAppSelector, useSettings } from "store/hooks";
|
||||
import { getMarkup, piconerosToXmr, satsToBtc } from "utils/conversionUtils";
|
||||
|
||||
|
@ -10,12 +10,18 @@ export function AmountWithUnit({
|
|||
fixedPrecision,
|
||||
exchangeRate,
|
||||
parenthesisText = null,
|
||||
labelStyles,
|
||||
amountStyles,
|
||||
disableTooltip = false,
|
||||
}: {
|
||||
amount: Amount;
|
||||
unit: string;
|
||||
fixedPrecision: number;
|
||||
exchangeRate?: Amount;
|
||||
parenthesisText?: string;
|
||||
labelStyles?: SxProps;
|
||||
amountStyles?: SxProps;
|
||||
disableTooltip?: boolean;
|
||||
}) {
|
||||
const [fetchFiatPrices, fiatCurrency] = useSettings((settings) => [
|
||||
settings.fetchFiatPrices,
|
||||
|
@ -29,12 +35,25 @@ export function AmountWithUnit({
|
|||
? `≈ ${(exchangeRate * amount).toFixed(2)} ${fiatCurrency}`
|
||||
: "";
|
||||
|
||||
const content = (
|
||||
<span>
|
||||
<Box sx={{ display: "inline", ...amountStyles }}>
|
||||
{amount != null ? amount.toFixed(fixedPrecision) : "?"}
|
||||
</Box>{" "}
|
||||
<Box sx={{ display: "inline", ...labelStyles }}>
|
||||
{unit}
|
||||
{parenthesisText != null ? ` (${parenthesisText})` : null}
|
||||
</Box>
|
||||
</span>
|
||||
);
|
||||
|
||||
if (disableTooltip) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip arrow title={title}>
|
||||
<span>
|
||||
{amount != null ? amount.toFixed(fixedPrecision) : "?"} {unit}
|
||||
{parenthesisText != null ? ` (${parenthesisText})` : null}
|
||||
</span>
|
||||
{content}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
@ -89,9 +108,15 @@ export function BitcoinAmount({ amount }: { amount: Amount }) {
|
|||
export function MoneroAmount({
|
||||
amount,
|
||||
fixedPrecision = 4,
|
||||
labelStyles,
|
||||
amountStyles,
|
||||
disableTooltip = false,
|
||||
}: {
|
||||
amount: Amount;
|
||||
fixedPrecision?: number;
|
||||
labelStyles?: SxProps;
|
||||
amountStyles?: SxProps;
|
||||
disableTooltip?: boolean;
|
||||
}) {
|
||||
const xmrRate = useAppSelector((state) => state.rates.xmrPrice);
|
||||
|
||||
|
@ -101,6 +126,9 @@ export function MoneroAmount({
|
|||
unit="XMR"
|
||||
fixedPrecision={fixedPrecision}
|
||||
exchangeRate={xmrRate}
|
||||
labelStyles={labelStyles}
|
||||
amountStyles={amountStyles}
|
||||
disableTooltip={disableTooltip}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -164,14 +192,23 @@ export function SatsAmount({ amount }: { amount: Amount }) {
|
|||
export function PiconeroAmount({
|
||||
amount,
|
||||
fixedPrecision = 8,
|
||||
labelStyles,
|
||||
amountStyles,
|
||||
disableTooltip = false,
|
||||
}: {
|
||||
amount: Amount;
|
||||
fixedPrecision?: number;
|
||||
labelStyles?: SxProps;
|
||||
amountStyles?: SxProps;
|
||||
disableTooltip?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<MoneroAmount
|
||||
amount={amount == null ? null : piconerosToXmr(amount)}
|
||||
fixedPrecision={fixedPrecision}
|
||||
labelStyles={labelStyles}
|
||||
amountStyles={amountStyles}
|
||||
disableTooltip={disableTooltip}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import { Box, Chip, Tooltip, Typography } from "@mui/material";
|
||||
import {
|
||||
AutoAwesome as AutoAwesomeIcon,
|
||||
CheckCircleOutline as CheckCircleOutlineIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
export default function ConfirmationsBadge({
|
||||
confirmations,
|
||||
}: {
|
||||
confirmations: number;
|
||||
}) {
|
||||
if (confirmations === 0) {
|
||||
return (
|
||||
<Chip
|
||||
icon={<AutoAwesomeIcon />}
|
||||
label="Published"
|
||||
color="secondary"
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
} else if (confirmations < 10) {
|
||||
const label = (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "end",
|
||||
gap: 0.4,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" sx={{ fontWeight: "bold" }}>
|
||||
{confirmations}
|
||||
</Typography>
|
||||
<Typography variant="caption">/10</Typography>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
return <Chip label={label} color="warning" size="small" />;
|
||||
} else {
|
||||
return (
|
||||
<Tooltip title={`${confirmations} Confirmations`}>
|
||||
<CheckCircleOutlineIcon
|
||||
sx={{ color: "text.secondary" }}
|
||||
fontSize="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,25 +1,8 @@
|
|||
import {
|
||||
Typography,
|
||||
Card,
|
||||
CardContent,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
Paper,
|
||||
Chip,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Stack,
|
||||
} from "@mui/material";
|
||||
import { OpenInNew as OpenInNewIcon } from "@mui/icons-material";
|
||||
import { open } from "@tauri-apps/plugin-shell";
|
||||
import { PiconeroAmount } from "../../../other/Units";
|
||||
import { getMoneroTxExplorerUrl } from "../../../../../utils/conversionUtils";
|
||||
import { isTestnet } from "store/config";
|
||||
import { Typography, Box, Paper } from "@mui/material";
|
||||
import { TransactionInfo } from "models/tauriModel";
|
||||
import _ from "lodash";
|
||||
import dayjs from "dayjs";
|
||||
import TransactionItem from "./TransactionItem";
|
||||
|
||||
interface TransactionHistoryProps {
|
||||
history?: {
|
||||
|
@ -27,76 +10,52 @@ interface TransactionHistoryProps {
|
|||
};
|
||||
}
|
||||
|
||||
interface TransactionGroup {
|
||||
date: string;
|
||||
displayDate: string;
|
||||
transactions: TransactionInfo[];
|
||||
}
|
||||
|
||||
// Component for displaying transaction history
|
||||
export default function TransactionHistory({
|
||||
history,
|
||||
}: TransactionHistoryProps) {
|
||||
if (!history || !history.transactions || history.transactions.length === 0) {
|
||||
return <Typography variant="h5">Transaction History</Typography>;
|
||||
return <Typography variant="h5">Transactions</Typography>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography variant="h5">Transaction History</Typography>
|
||||
const transactions = history.transactions;
|
||||
|
||||
<TableContainer component={Paper} variant="outlined">
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Amount</TableCell>
|
||||
<TableCell>Fee</TableCell>
|
||||
<TableCell align="right">Confirmations</TableCell>
|
||||
<TableCell align="center">Explorer</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{[...history.transactions]
|
||||
.sort((a, b) => a.confirmations - b.confirmations)
|
||||
.map((tx, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<PiconeroAmount amount={tx.amount} />
|
||||
<Chip
|
||||
label={tx.direction === "In" ? "Received" : "Sent"}
|
||||
color={tx.direction === "In" ? "success" : "default"}
|
||||
size="small"
|
||||
/>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<PiconeroAmount amount={tx.fee} />
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Chip
|
||||
label={tx.confirmations}
|
||||
color={tx.confirmations >= 10 ? "success" : "warning"}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
{tx.tx_hash && (
|
||||
<Tooltip title="View on block explorer">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
const url = getMoneroTxExplorerUrl(
|
||||
tx.tx_hash,
|
||||
isTestnet(),
|
||||
);
|
||||
open(url);
|
||||
}}
|
||||
>
|
||||
<OpenInNewIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
// Group transactions by date using dayjs and lodash
|
||||
const transactionGroups: TransactionGroup[] = _(transactions)
|
||||
.groupBy((tx) => dayjs(tx.timestamp * 1000).format("YYYY-MM-DD")) // Convert Unix timestamp to date string
|
||||
.map((txs, dateKey) => ({
|
||||
date: dateKey,
|
||||
displayDate: dayjs(dateKey).format("MMMM D, YYYY"), // Human-readable format
|
||||
transactions: _.orderBy(txs, ["timestamp"], ["desc"]), // Sort transactions within group by newest first
|
||||
}))
|
||||
.orderBy(["date"], ["desc"]) // Sort groups by newest date first
|
||||
.value();
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ mb: 2 }}>
|
||||
Transactions
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||
{transactionGroups.map((group) => (
|
||||
<Box key={group.date}>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 1 }}>
|
||||
{group.displayDate}
|
||||
</Typography>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
{group.transactions.map((tx) => (
|
||||
<TransactionItem key={tx.tx_hash} transaction={tx} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
import {
|
||||
Box,
|
||||
Chip,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { TransactionDirection, TransactionInfo } from "models/tauriModel";
|
||||
import {
|
||||
CallReceived as IncomingIcon,
|
||||
MoreVert as MoreVertIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { CallMade as OutgoingIcon } from "@mui/icons-material";
|
||||
import {
|
||||
FiatPiconeroAmount,
|
||||
PiconeroAmount,
|
||||
} from "renderer/components/other/Units";
|
||||
import ConfirmationsBadge from "./ConfirmationsBadge";
|
||||
import { getMoneroTxExplorerUrl } from "utils/conversionUtils";
|
||||
import { isTestnet } from "store/config";
|
||||
import { open } from "@tauri-apps/plugin-shell";
|
||||
import dayjs from "dayjs";
|
||||
import { useState } from "react";
|
||||
|
||||
interface TransactionItemProps {
|
||||
transaction: TransactionInfo;
|
||||
}
|
||||
|
||||
export default function TransactionItem({ transaction }: TransactionItemProps) {
|
||||
const isIncoming = transaction.direction === TransactionDirection.In;
|
||||
const displayDate = dayjs(transaction.timestamp * 1000).format(
|
||||
"MMM DD YYYY, HH:mm",
|
||||
);
|
||||
|
||||
const amountStyles = isIncoming
|
||||
? { color: "success.tint" }
|
||||
: { color: "error.tint" };
|
||||
|
||||
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const menuOpen = Boolean(menuAnchorEl);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
p: 0.5,
|
||||
backgroundColor: "grey.800",
|
||||
borderRadius: "100%",
|
||||
height: 40,
|
||||
aspectRatio: 1,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{isIncoming ? <IncomingIcon /> : <OutgoingIcon />}
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "min-content max-content",
|
||||
columnGap: 0.5,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
opacity: !isIncoming ? 1 : 0,
|
||||
gridArea: "1 / 1",
|
||||
fontWeight: "bold",
|
||||
...amountStyles,
|
||||
}}
|
||||
>
|
||||
‐
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ gridArea: "1 / 2", fontWeight: "bold", ...amountStyles }}
|
||||
>
|
||||
<PiconeroAmount
|
||||
amount={transaction.amount}
|
||||
labelStyles={{ fontSize: 14, ml: -0.3 }}
|
||||
disableTooltip
|
||||
/>
|
||||
</Typography>
|
||||
<Typography variant="caption" sx={{ gridArea: "2 / 2" }}>
|
||||
<FiatPiconeroAmount amount={transaction.amount} />
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="text.secondary"
|
||||
sx={{ fontSize: 14 }}
|
||||
>
|
||||
{displayDate}
|
||||
</Typography>
|
||||
<ConfirmationsBadge confirmations={transaction.confirmations} />
|
||||
<IconButton
|
||||
onClick={(event) => {
|
||||
setMenuAnchorEl(event.currentTarget);
|
||||
}}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
anchorEl={menuAnchorEl}
|
||||
open={menuOpen}
|
||||
onClose={() => setMenuAnchorEl(null)}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(transaction.tx_hash);
|
||||
setMenuAnchorEl(null);
|
||||
}}
|
||||
>
|
||||
<Typography>Copy Transaction ID</Typography>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
open(getMoneroTxExplorerUrl(transaction.tx_hash, isTestnet()));
|
||||
setMenuAnchorEl(null);
|
||||
}}
|
||||
>
|
||||
<Typography>View on Explorer</Typography>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -72,6 +72,7 @@ export default function WalletOverview({
|
|||
<PiconeroAmount
|
||||
amount={parseFloat(balance.unlocked_balance)}
|
||||
fixedPrecision={4}
|
||||
disableTooltip
|
||||
/>
|
||||
</Typography>
|
||||
<Typography
|
||||
|
|
|
@ -18,6 +18,17 @@ declare module "@mui/material/Button" {
|
|||
}
|
||||
}
|
||||
|
||||
// Extend the palette to include custom color properties
|
||||
declare module "@mui/material/styles" {
|
||||
interface PaletteColor {
|
||||
tint?: string;
|
||||
}
|
||||
|
||||
interface PaletteColorOptions {
|
||||
tint?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export enum Theme {
|
||||
Light = "light",
|
||||
Dark = "dark",
|
||||
|
@ -93,6 +104,19 @@ const baseTheme: ThemeOptions = {
|
|||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
props: { size: "tiny" },
|
||||
style: {
|
||||
fontSize: "0.75rem",
|
||||
fontWeight: 500,
|
||||
padding: "4px 8px",
|
||||
minHeight: "24px",
|
||||
minWidth: "auto",
|
||||
lineHeight: 1.2,
|
||||
textTransform: "none",
|
||||
borderRadius: "4px",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
MuiChip: {
|
||||
|
@ -142,6 +166,14 @@ const darkTheme = createTheme({
|
|||
main: "#f4511e", // Monero orange
|
||||
},
|
||||
secondary: indigo,
|
||||
error: {
|
||||
main: "#f44336",
|
||||
tint: "#e58686",
|
||||
},
|
||||
success: {
|
||||
main: "#4caf50",
|
||||
tint: "#70c491",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -153,6 +185,14 @@ const lightTheme = createTheme({
|
|||
main: "#f4511e", // Monero orange
|
||||
},
|
||||
secondary: indigo,
|
||||
error: {
|
||||
main: "#f44336",
|
||||
tint: "#ff5252",
|
||||
},
|
||||
success: {
|
||||
main: "#4caf50",
|
||||
tint: "#4caf50",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue