mirror of
https://github.com/comit-network/xmr-btc-swap.git
synced 2025-12-17 17:44:02 -05:00
refactor(gui): Update MUI to v7 (#383)
* task(gui): update to mui v5 * task(gui): use sx prop instead of system props * task(gui): update to mui v6 and replace makeStyles with sx prop * task(gui): update to mui v7 * task(gui): update react * fix(gui): fix import * task(gui): adjust theme and few components to fix migration introduced styling errors * fix(gui): animation issues with text field animations * fix(gui): remove 'darker' theme and make 'dark' theme the default - with the new update 'dark' theme is already quite dark and therefore a 'darker' theme not necessary - the default theme is set to 'dark' now in settings initialization * feat(tooling): Upgrade dprint to 0.50.0, eslint config, prettier, justfile commands - Upgrade dprint to 0.50.0 - Use sane default eslint config (fairly permissive) - `dprint fmt` now runs prettier for the `src-gui` folder - Added `check_gui_eslint`, `check_gui_tsc` and `check_gui` commands * refactor: fix a few eslint errors * dprint fmt * fix tsc complains * nitpick: small spacing issue --------- Co-authored-by: Binarybaron <binarybaron@protonmail.com> Co-authored-by: Mohan <86064887+binarybaron@users.noreply.github.com>
This commit is contained in:
parent
2ba69ba340
commit
430a22fbf6
169 changed files with 12883 additions and 3950 deletions
|
|
@ -1,25 +1,21 @@
|
|||
import { Box, makeStyles } from "@material-ui/core";
|
||||
import { Box } from "@mui/material";
|
||||
import FeedbackInfoBox from "../help/FeedbackInfoBox";
|
||||
import ConversationsBox from "../help/ConversationsBox";
|
||||
import ContactInfoBox from "../help/ContactInfoBox";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
outer: {
|
||||
display: "flex",
|
||||
gap: theme.spacing(2),
|
||||
flexDirection: "column",
|
||||
paddingBottom: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function FeedbackPage() {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Box className={classes.outer}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: 2,
|
||||
flexDirection: "column",
|
||||
paddingBottom: 2,
|
||||
}}
|
||||
>
|
||||
<FeedbackInfoBox />
|
||||
<ConversationsBox />
|
||||
<ContactInfoBox />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,13 @@
|
|||
import { Box, Button, makeStyles, Typography } from "@material-ui/core";
|
||||
import { Box, Button, Typography } from "@mui/material";
|
||||
import { open } from "@tauri-apps/plugin-shell";
|
||||
import InfoBox from "../../modal/swap/InfoBox";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
spacedBox: {
|
||||
display: "flex",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
const GITHUB_ISSUE_URL =
|
||||
"https://github.com/UnstoppableSwap/core/issues/new/choose";
|
||||
const MATRIX_ROOM_URL = "https://matrix.to/#/#unstoppableswap:matrix.org";
|
||||
export const DISCORD_URL = "https://discord.gg/aqSyyJ35UW";
|
||||
|
||||
export default function ContactInfoBox() {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<InfoBox
|
||||
title="Get in touch"
|
||||
|
|
@ -27,7 +18,7 @@ export default function ContactInfoBox() {
|
|||
</Typography>
|
||||
}
|
||||
additionalContent={
|
||||
<Box className={classes.spacedBox}>
|
||||
<Box sx={{ display: "flex", gap: 1 }}>
|
||||
<Button variant="outlined" onClick={() => open(GITHUB_ISSUE_URL)}>
|
||||
Open GitHub issue
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { useState, useEffect, useMemo } from "react";
|
|||
import {
|
||||
Box,
|
||||
Typography,
|
||||
makeStyles,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
|
|
@ -25,45 +24,28 @@ import {
|
|||
ListItem,
|
||||
ListItemIcon,
|
||||
Link,
|
||||
} from "@material-ui/core";
|
||||
import ChatIcon from '@material-ui/icons/Chat';
|
||||
import SendIcon from '@material-ui/icons/Send';
|
||||
} from "@mui/material";
|
||||
import ChatIcon from "@mui/icons-material/Chat";
|
||||
import SendIcon from "@mui/icons-material/Send";
|
||||
import InfoBox from "renderer/components/modal/swap/InfoBox";
|
||||
import TruncatedText from "renderer/components/other/TruncatedText";
|
||||
import clsx from 'clsx';
|
||||
import { useAppSelector, useAppDispatch, useUnreadMessagesCount } from "store/hooks";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
useAppSelector,
|
||||
useAppDispatch,
|
||||
useUnreadMessagesCount,
|
||||
} from "store/hooks";
|
||||
import { markMessagesAsSeen } from "store/features/conversationsSlice";
|
||||
import { appendFeedbackMessageViaHttp, fetchAllConversations } from "renderer/api";
|
||||
import {
|
||||
appendFeedbackMessageViaHttp,
|
||||
fetchAllConversations,
|
||||
} from "renderer/api";
|
||||
import { useSnackbar } from "notistack";
|
||||
import logger from "utils/logger";
|
||||
import AttachmentIcon from '@material-ui/icons/Attachment';
|
||||
import AttachmentIcon from "@mui/icons-material/Attachment";
|
||||
import { Message, PrimitiveDateTimeString } from "models/apiModel";
|
||||
import { formatDateTime } from "utils/conversionUtils";
|
||||
|
||||
// Styles
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
content: { display: "flex", flexDirection: "column", alignItems: "flex-start", gap: theme.spacing(2) },
|
||||
tableContainer: { maxHeight: 300 },
|
||||
dialogContent: { display: 'flex', flexDirection: 'column' },
|
||||
messagesContainer: { flexGrow: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: theme.spacing(1), maxHeight: 400, padding: theme.spacing(1) },
|
||||
messageRow: { display: 'flex', marginTop: theme.spacing(1) },
|
||||
staffRow: { justifyContent: 'flex-start' },
|
||||
userRow: { justifyContent: 'flex-end' },
|
||||
messageBubble: { padding: theme.spacing(1.5), borderRadius: theme.shape.borderRadius * 2, maxWidth: '75%', wordBreak: 'break-word', boxShadow: theme.shadows[1] },
|
||||
staffBubble: { border: `1px solid ${theme.palette.divider}`, color: theme.palette.text.primary, borderRadius: theme.spacing(2) },
|
||||
userBubble: { backgroundColor: theme.palette.primary.main, color: theme.palette.primary.contrastText, borderRadius: theme.spacing(2) },
|
||||
timestamp: { marginTop: theme.spacing(0.5), fontSize: '0.75rem', opacity: 0.7, textAlign: 'right' },
|
||||
inputArea: { flexShrink: 0, marginTop: theme.spacing(2) },
|
||||
attachmentList: {
|
||||
marginTop: theme.spacing(1),
|
||||
marginBottom: theme.spacing(1),
|
||||
paddingLeft: theme.spacing(2),
|
||||
},
|
||||
attachmentItem: {
|
||||
paddingTop: theme.spacing(0.5),
|
||||
paddingBottom: theme.spacing(0.5),
|
||||
}
|
||||
}));
|
||||
import { Theme } from "renderer/components/theme";
|
||||
|
||||
// Hook: sorted feedback IDs by latest activity, then unread
|
||||
function useSortedFeedbackIds() {
|
||||
|
|
@ -73,25 +55,31 @@ function useSortedFeedbackIds() {
|
|||
return useMemo(() => {
|
||||
const arr = ids.map((id) => {
|
||||
const msgs = conv[id] || [];
|
||||
const unread = msgs.filter((m) => m.is_from_staff && !seen.has(m.id.toString())).length;
|
||||
const unread = msgs.filter(
|
||||
(m) => m.is_from_staff && !seen.has(m.id.toString()),
|
||||
).length;
|
||||
const latest = msgs.reduce((d, m) => {
|
||||
try {
|
||||
const formattedDate = formatDateTime(m.created_at);
|
||||
if (formattedDate.startsWith("Invalid")) return d;
|
||||
const t = new Date(formattedDate).getTime();
|
||||
return isNaN(t) ? d : Math.max(d, t);
|
||||
} catch(e) { return d; }
|
||||
} catch (e) {
|
||||
return d;
|
||||
}
|
||||
}, 0);
|
||||
return { id, unread, latest };
|
||||
});
|
||||
arr.sort((a, b) => b.latest - a.latest || (b.unread > 0 ? 1 : 0) - (a.unread > 0 ? 1 : 0));
|
||||
arr.sort(
|
||||
(a, b) =>
|
||||
b.latest - a.latest || (b.unread > 0 ? 1 : 0) - (a.unread > 0 ? 1 : 0),
|
||||
);
|
||||
return arr.map((x) => x.id);
|
||||
}, [ids, conv, seen]);
|
||||
}
|
||||
|
||||
// Main component
|
||||
export default function ConversationsBox() {
|
||||
const classes = useStyles();
|
||||
const sortedIds = useSortedFeedbackIds();
|
||||
const [openId, setOpenId] = useState<string | null>(null);
|
||||
|
||||
|
|
@ -106,25 +94,57 @@ export default function ConversationsBox() {
|
|||
icon={null}
|
||||
loading={false}
|
||||
mainContent={
|
||||
<Box className={classes.content}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2">
|
||||
View your past feedback submissions and any replies from the development team.
|
||||
View your past feedback submissions and any replies from the
|
||||
development team.
|
||||
</Typography>
|
||||
{sortedIds.length === 0 ? (
|
||||
<Typography variant="body2">No feedback submitted yet.</Typography>
|
||||
) : (
|
||||
<TableContainer component={Paper} className={classes.tableContainer}>
|
||||
<TableContainer component={Paper} sx={{ maxHeight: 300 }}>
|
||||
<Table stickyHeader size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell style={{ width: '25%' }}>Last Message</TableCell>
|
||||
<TableCell style={{ width: '60%' }}>Preview</TableCell>
|
||||
<TableCell align="right" style={{ width: '15%' }} />
|
||||
<TableCell
|
||||
sx={(theme) => ({
|
||||
width: "25%",
|
||||
backgroundColor: theme.palette.grey[900],
|
||||
})}
|
||||
>
|
||||
Last Message
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={(theme) => ({
|
||||
width: "60%",
|
||||
backgroundColor: theme.palette.grey[900],
|
||||
})}
|
||||
>
|
||||
Preview
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="right"
|
||||
sx={(theme) => ({
|
||||
width: "15%",
|
||||
backgroundColor: theme.palette.grey[900],
|
||||
})}
|
||||
/>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{sortedIds.map((id) => (
|
||||
<ConversationRow key={id} feedbackId={id} onOpen={setOpenId} />
|
||||
<ConversationRow
|
||||
key={id}
|
||||
feedbackId={id}
|
||||
onOpen={setOpenId}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
|
@ -146,8 +166,16 @@ export default function ConversationsBox() {
|
|||
}
|
||||
|
||||
// Single row
|
||||
function ConversationRow({ feedbackId, onOpen }: { feedbackId: string, onOpen: (id: string) => void }) {
|
||||
const msgs = useAppSelector((s) => s.conversations.conversations[feedbackId] || []);
|
||||
function ConversationRow({
|
||||
feedbackId,
|
||||
onOpen,
|
||||
}: {
|
||||
feedbackId: string;
|
||||
onOpen: (id: string) => void;
|
||||
}) {
|
||||
const msgs = useAppSelector(
|
||||
(s) => s.conversations.conversations[feedbackId] || [],
|
||||
);
|
||||
const unread = useUnreadMessagesCount(feedbackId);
|
||||
const sorted = useMemo(
|
||||
() =>
|
||||
|
|
@ -162,13 +190,15 @@ function ConversationRow({ feedbackId, onOpen }: { feedbackId: string, onOpen: (
|
|||
if (isNaN(dateA)) return 1;
|
||||
if (isNaN(dateB)) return -1;
|
||||
return dateB - dateA;
|
||||
} catch (e) { return 0; }
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}),
|
||||
[msgs]
|
||||
[msgs],
|
||||
);
|
||||
const lastMsg = sorted[0];
|
||||
const time = lastMsg ? formatDateTime(lastMsg.created_at) : '-';
|
||||
const content = lastMsg ? lastMsg.content : 'No messages yet';
|
||||
const time = lastMsg ? formatDateTime(lastMsg.created_at) : "-";
|
||||
const content = lastMsg ? lastMsg.content : "No messages yet";
|
||||
const preview = (() => {
|
||||
return content;
|
||||
})();
|
||||
|
|
@ -176,15 +206,24 @@ function ConversationRow({ feedbackId, onOpen }: { feedbackId: string, onOpen: (
|
|||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell style={{ width: '25%' }}>{time}</TableCell>
|
||||
<TableCell style={{ width: '60%' }}>
|
||||
<TableCell style={{ width: "25%" }}>{time}</TableCell>
|
||||
<TableCell style={{ width: "60%" }}>
|
||||
"<TruncatedText limit={30}>{preview}</TruncatedText>"
|
||||
</TableCell>
|
||||
<TableCell align="right" style={{ width: '15%' }}>
|
||||
<TableCell align="right" style={{ width: "15%" }}>
|
||||
<Badge badgeContent={unread} color="primary" overlap="rectangular">
|
||||
<Tooltip title={hasStaff ? 'Open Conversation' : 'No developer has responded'} arrow>
|
||||
<Tooltip
|
||||
title={
|
||||
hasStaff ? "Open Conversation" : "No developer has responded"
|
||||
}
|
||||
arrow
|
||||
>
|
||||
<span>
|
||||
<IconButton size="small" onClick={() => onOpen(feedbackId)} disabled={!hasStaff}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => onOpen(feedbackId)}
|
||||
disabled={!hasStaff}
|
||||
>
|
||||
<ChatIcon />
|
||||
</IconButton>
|
||||
</span>
|
||||
|
|
@ -196,138 +235,117 @@ function ConversationRow({ feedbackId, onOpen }: { feedbackId: string, onOpen: (
|
|||
}
|
||||
|
||||
// Modal
|
||||
function ConversationModal({ open, onClose, feedbackId }: { open: boolean, onClose: () => void, feedbackId: string }) {
|
||||
const classes = useStyles();
|
||||
function ConversationModal({
|
||||
open,
|
||||
onClose,
|
||||
feedbackId,
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
feedbackId: string;
|
||||
}) {
|
||||
const dispatch = useAppDispatch();
|
||||
const msgs = useAppSelector(
|
||||
(s) => s.conversations.conversations[feedbackId] || [],
|
||||
);
|
||||
const [newMessage, setNewMessage] = useState("");
|
||||
const [sending, setSending] = useState(false);
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const msgs = useAppSelector((s): Message[] => s.conversations.conversations[feedbackId] || []);
|
||||
const seen = useAppSelector((s) => new Set(s.conversations.seenMessages));
|
||||
const [text, setText] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Mark messages as seen when modal opens
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const unseen = msgs.filter((m) => !seen.has(m.id.toString()));
|
||||
if (unseen.length) dispatch(markMessagesAsSeen(unseen));
|
||||
if (open && msgs.length > 0) {
|
||||
const unreadMessages = msgs.filter((m) => m.is_from_staff);
|
||||
if (unreadMessages.length > 0) {
|
||||
dispatch(markMessagesAsSeen(unreadMessages));
|
||||
}
|
||||
}
|
||||
}, [open, msgs, seen, dispatch]);
|
||||
}, [open, msgs, dispatch]);
|
||||
|
||||
const sorted = useMemo(
|
||||
// Sort messages chronologically
|
||||
const sortedMsgs = useMemo(
|
||||
() =>
|
||||
[...msgs].sort((a, b) => {
|
||||
try {
|
||||
const formattedDateA = formatDateTime(a.created_at);
|
||||
const formattedDateB = formatDateTime(b.created_at);
|
||||
if (formattedDateA.startsWith("Invalid")) return 1;
|
||||
if (formattedDateB.startsWith("Invalid")) return -1;
|
||||
if (formattedDateA.startsWith("Invalid")) return -1;
|
||||
if (formattedDateB.startsWith("Invalid")) return 1;
|
||||
const dateA = new Date(formattedDateA).getTime();
|
||||
const dateB = new Date(formattedDateB).getTime();
|
||||
if (isNaN(dateA)) return 1;
|
||||
if (isNaN(dateB)) return -1;
|
||||
return dateB - dateA;
|
||||
} catch(e) { return 0; }
|
||||
if (isNaN(dateA)) return -1;
|
||||
if (isNaN(dateB)) return 1;
|
||||
return dateA - dateB;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}),
|
||||
[msgs]
|
||||
[msgs],
|
||||
);
|
||||
|
||||
const sendMessage = async () => {
|
||||
if (!text.trim()) return;
|
||||
setLoading(true);
|
||||
if (!newMessage.trim() || sending) return;
|
||||
setSending(true);
|
||||
try {
|
||||
await appendFeedbackMessageViaHttp(feedbackId, text);
|
||||
setText('');
|
||||
enqueueSnackbar('Message sent successfully!', { variant: 'success' });
|
||||
await appendFeedbackMessageViaHttp(feedbackId, newMessage.trim());
|
||||
setNewMessage("");
|
||||
enqueueSnackbar("Message sent successfully!", { variant: "success" });
|
||||
// Fetch updated conversations
|
||||
fetchAllConversations();
|
||||
} catch (e) {
|
||||
enqueueSnackbar('Failed to send message. Please try again.', { variant: 'error' });
|
||||
} catch (error) {
|
||||
logger.error("Error sending message:", error);
|
||||
enqueueSnackbar("Failed to send message. Please try again.", {
|
||||
variant: "error",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth scroll="paper">
|
||||
<DialogTitle>Conversation <TruncatedText limit={8} truncateMiddle>{feedbackId}</TruncatedText></DialogTitle>
|
||||
<DialogContent dividers className={classes.dialogContent}>
|
||||
{sorted.length === 0 ? (
|
||||
<Typography variant="body2">No messages in this conversation.</Typography>
|
||||
) : (
|
||||
<Box className={classes.messagesContainer}>
|
||||
{sorted.map((m: Message) => {
|
||||
const raw = m.content;
|
||||
return (
|
||||
<Box
|
||||
key={m.id}
|
||||
className={clsx(
|
||||
classes.messageRow,
|
||||
m.is_from_staff ? classes.staffRow : classes.userRow
|
||||
)}
|
||||
>
|
||||
<Box className={clsx(
|
||||
classes.messageBubble,
|
||||
m.is_from_staff ? classes.staffBubble : classes.userBubble
|
||||
)}
|
||||
>
|
||||
<Typography variant="body1" style={{ whiteSpace: 'pre-wrap' }}>
|
||||
{raw}
|
||||
</Typography>
|
||||
{m.attachments && m.attachments.length > 0 && (
|
||||
<List dense disablePadding className={classes.attachmentList}>
|
||||
{m.attachments.map((att) => (
|
||||
<ListItem key={att.id} className={classes.attachmentItem}>
|
||||
<ListItemIcon style={{ minWidth: 'auto', marginRight: '8px' }}>
|
||||
<AttachmentIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<Link
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
alert(`Attachment Key: ${att.key}\n\nContent:\n${att.content}`);
|
||||
}}
|
||||
variant="body2"
|
||||
color="inherit"
|
||||
underline="none"
|
||||
>
|
||||
{att.key}
|
||||
</Link>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
<Typography variant="caption" className={classes.timestamp}>
|
||||
{m.is_from_staff ? 'Developer' : 'You'} · {formatDateTime(m.created_at)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
<Box className={classes.inputArea}>
|
||||
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>Conversation</DialogTitle>
|
||||
<DialogContent sx={{ display: "flex", flexDirection: "column" }}>
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
overflowY: "auto",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1,
|
||||
maxHeight: 400,
|
||||
padding: 1,
|
||||
}}
|
||||
>
|
||||
{sortedMsgs.map((msg) => (
|
||||
<MessageBubble key={msg.id} message={msg} />
|
||||
))}
|
||||
</Box>
|
||||
<Box sx={{ flexShrink: 0, marginTop: 2 }}>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
placeholder="Type your message..."
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
disabled={loading}
|
||||
multiline
|
||||
rowsMax={4}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
}
|
||||
}}
|
||||
rows={3}
|
||||
value={newMessage}
|
||||
onChange={(e) => setNewMessage(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Type your message here..."
|
||||
disabled={sending}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
color="primary"
|
||||
onClick={sendMessage}
|
||||
disabled={!text.trim() || loading}
|
||||
disabled={!newMessage.trim() || sending}
|
||||
>
|
||||
{loading ? <CircularProgress size={24} /> : <SendIcon />}
|
||||
{sending ? <CircularProgress size={20} /> : <SendIcon />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
),
|
||||
|
|
@ -336,10 +354,86 @@ function ConversationModal({ open, onClose, feedbackId }: { open: boolean, onClo
|
|||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} variant="outlined">
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
// Message bubble component
|
||||
function MessageBubble({ message }: { message: Message }) {
|
||||
const isStaff = message.is_from_staff;
|
||||
const time = formatDateTime(message.created_at);
|
||||
|
||||
const attachments = message.attachments || [];
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
marginTop: 1,
|
||||
justifyContent: isStaff ? "flex-start" : "flex-end",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
padding: 1.5,
|
||||
borderRadius:
|
||||
typeof theme.shape.borderRadius === "number"
|
||||
? theme.shape.borderRadius * 2
|
||||
: 8,
|
||||
maxWidth: "75%",
|
||||
wordBreak: "break-word",
|
||||
boxShadow: theme.shadows[1],
|
||||
...(isStaff
|
||||
? {
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
color: theme.palette.text.primary,
|
||||
borderRadius: 2,
|
||||
}
|
||||
: {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
borderRadius: 2,
|
||||
}),
|
||||
})}
|
||||
>
|
||||
<Typography variant="body2">{message.content}</Typography>
|
||||
{attachments.length > 0 && (
|
||||
<List sx={{ marginTop: 1, marginBottom: 1, paddingLeft: 2 }}>
|
||||
{attachments.map((att, idx) => (
|
||||
<ListItem key={idx} sx={{ paddingTop: 0.5, paddingBottom: 0.5 }}>
|
||||
<ListItemIcon>
|
||||
<AttachmentIcon />
|
||||
</ListItemIcon>
|
||||
<Link
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
alert(
|
||||
`Attachment Key: ${att.key}\n\nContent:\n${att.content}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
{att.key}
|
||||
</Link>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
marginTop: 0.5,
|
||||
fontSize: "0.75rem",
|
||||
opacity: 0.7,
|
||||
textAlign: "right",
|
||||
display: "block",
|
||||
}}
|
||||
>
|
||||
{time}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,50 +1,41 @@
|
|||
import { Box, makeStyles } from "@material-ui/core";
|
||||
import FolderOpenIcon from "@material-ui/icons/FolderOpen";
|
||||
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
|
||||
import { Box } from "@mui/material";
|
||||
import FolderOpenIcon from "@mui/icons-material/FolderOpen";
|
||||
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||
import { useAppSelector } from "store/hooks";
|
||||
import InfoBox from "../../modal/swap/InfoBox";
|
||||
import CliLogsBox from "../../other/RenderedCliLog";
|
||||
import { getDataDir, initializeContext } from "renderer/rpc";
|
||||
import { relaunch } from "@tauri-apps/plugin-process";
|
||||
import RotateLeftIcon from "@material-ui/icons/RotateLeft";
|
||||
import RotateLeftIcon from "@mui/icons-material/RotateLeft";
|
||||
import { revealItemInDir } from "@tauri-apps/plugin-opener";
|
||||
import { TauriContextStatusEvent } from "models/tauriModel";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
actionsOuter: {
|
||||
display: "flex",
|
||||
gap: theme.spacing(1),
|
||||
alignItems: "center",
|
||||
},
|
||||
}));
|
||||
|
||||
export default function DaemonControlBox() {
|
||||
const classes = useStyles();
|
||||
const logs = useAppSelector((s) => s.rpc.logs);
|
||||
|
||||
// The daemon can be manually started if it has failed or if it has not been started yet
|
||||
const canContextBeManuallyStarted = useAppSelector(
|
||||
(s) => s.rpc.status === TauriContextStatusEvent.Failed || s.rpc.status === null,
|
||||
(s) =>
|
||||
s.rpc.status === TauriContextStatusEvent.Failed || s.rpc.status === null,
|
||||
);
|
||||
const isContextInitializing = useAppSelector(
|
||||
(s) => s.rpc.status === TauriContextStatusEvent.Initializing,
|
||||
);
|
||||
|
||||
const stringifiedDaemonStatus = useAppSelector((s) => s.rpc.status ?? "not started");
|
||||
const stringifiedDaemonStatus = useAppSelector(
|
||||
(s) => s.rpc.status ?? "not started",
|
||||
);
|
||||
|
||||
return (
|
||||
<InfoBox
|
||||
id="daemon-control-box"
|
||||
title={`Daemon Controller (${stringifiedDaemonStatus})`}
|
||||
mainContent={
|
||||
<CliLogsBox
|
||||
label="Logs (current session only)"
|
||||
logs={logs}
|
||||
/>
|
||||
<CliLogsBox label="Logs (current session only)" logs={logs} />
|
||||
}
|
||||
additionalContent={
|
||||
<Box className={classes.actionsOuter}>
|
||||
<Box sx={{ display: "flex", gap: 1, alignItems: "center" }}>
|
||||
<PromiseInvokeButton
|
||||
variant="contained"
|
||||
endIcon={<PlayArrowIcon />}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Link, Typography } from "@material-ui/core";
|
||||
import { Link, Typography } from "@mui/material";
|
||||
import MoneroIcon from "../../icons/MoneroIcon";
|
||||
import DepositAddressInfoBox from "../../modal/swap/DepositAddressInfoBox";
|
||||
|
||||
|
|
@ -14,11 +14,20 @@ export default function DonateInfoBox() {
|
|||
additionalContent={
|
||||
<Typography variant="subtitle2">
|
||||
<p>
|
||||
As part of the Monero Community Crowdfunding System (CCS), we received funding for 6 months of full-time development by
|
||||
generous donors from the Monero community (<Link href="https://ccs.getmonero.org/proposals/mature-atomic-swaps-ecosystem.html" target="_blank">link</Link>).
|
||||
As part of the Monero Community Crowdfunding System (CCS), we
|
||||
received funding for 6 months of full-time development by generous
|
||||
donors from the Monero community (
|
||||
<Link
|
||||
href="https://ccs.getmonero.org/proposals/mature-atomic-swaps-ecosystem.html"
|
||||
target="_blank"
|
||||
>
|
||||
link
|
||||
</Link>
|
||||
).
|
||||
</p>
|
||||
<p>
|
||||
If you want to support our effort event further, you can do so at this address.
|
||||
If you want to support our effort event further, you can do so at
|
||||
this address.
|
||||
</p>
|
||||
</Typography>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import {
|
||||
Box,
|
||||
Typography,
|
||||
makeStyles,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
|
|
@ -9,7 +8,7 @@ import {
|
|||
Button,
|
||||
Link,
|
||||
DialogContentText,
|
||||
} from "@material-ui/core";
|
||||
} from "@mui/material";
|
||||
import InfoBox from "renderer/components/modal/swap/InfoBox";
|
||||
import { useState } from "react";
|
||||
import { getWalletDescriptor } from "renderer/rpc";
|
||||
|
|
@ -17,18 +16,9 @@ import { ExportBitcoinWalletResponse } from "models/tauriModel";
|
|||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||
import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
content: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
gap: theme.spacing(2),
|
||||
}
|
||||
}));
|
||||
|
||||
export default function ExportDataBox() {
|
||||
const classes = useStyles();
|
||||
const [walletDescriptor, setWalletDescriptor] = useState<ExportBitcoinWalletResponse | null>(null);
|
||||
const [walletDescriptor, setWalletDescriptor] =
|
||||
useState<ExportBitcoinWalletResponse | null>(null);
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setWalletDescriptor(null);
|
||||
|
|
@ -40,9 +30,18 @@ export default function ExportDataBox() {
|
|||
icon={null}
|
||||
loading={false}
|
||||
mainContent={
|
||||
<Box className={classes.content}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2">
|
||||
You can export the wallet descriptor of the interal Bitcoin wallet for backup or recovery purposes. Please make sure to store it securely.
|
||||
You can export the wallet descriptor of the interal Bitcoin wallet
|
||||
for backup or recovery purposes. Please make sure to store it
|
||||
securely.
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
|
|
@ -78,7 +77,9 @@ function WalletDescriptorModal({
|
|||
onClose: () => void;
|
||||
walletDescriptor: ExportBitcoinWalletResponse;
|
||||
}) {
|
||||
const parsedDescriptor = JSON.parse(walletDescriptor.wallet_descriptor["descriptor"]);
|
||||
const parsedDescriptor = JSON.parse(
|
||||
walletDescriptor.wallet_descriptor["descriptor"],
|
||||
);
|
||||
const stringifiedDescriptor = JSON.stringify(parsedDescriptor, null, 4);
|
||||
|
||||
return (
|
||||
|
|
@ -88,14 +89,22 @@ function WalletDescriptorModal({
|
|||
<DialogContentText>
|
||||
<ul style={{ marginTop: 0 }}>
|
||||
<li>
|
||||
The text below contains the wallet descriptor of the internal Bitcoin wallet. It contains your private key and can be used to derive your wallet. It should thus be stored securely.
|
||||
The text below contains the wallet descriptor of the internal
|
||||
Bitcoin wallet. It contains your private key and can be used to
|
||||
derive your wallet. It should thus be stored securely.
|
||||
</li>
|
||||
<li>
|
||||
It can be imported into other Bitcoin wallets or services that support the descriptor format.
|
||||
It can be imported into other Bitcoin wallets or services that
|
||||
support the descriptor format.
|
||||
</li>
|
||||
<li>
|
||||
For more information on what to do with the descriptor, see our
|
||||
{" "}<Link href="https://github.com/UnstoppableSwap/core/blob/master/dev-docs/asb/README.md#exporting-the-bitcoin-wallet-descriptor" target="_blank">documentation</Link>
|
||||
For more information on what to do with the descriptor, see our{" "}
|
||||
<Link
|
||||
href="https://github.com/UnstoppableSwap/core/blob/master/dev-docs/asb/README.md#exporting-the-bitcoin-wallet-descriptor"
|
||||
target="_blank"
|
||||
>
|
||||
documentation
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</DialogContentText>
|
||||
|
|
@ -112,4 +121,4 @@ function WalletDescriptorModal({
|
|||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Button, Typography } from "@material-ui/core";
|
||||
import { Button, Typography } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import FeedbackDialog from "../../modal/feedback/FeedbackDialog";
|
||||
import InfoBox from "../../modal/swap/InfoBox";
|
||||
|
|
@ -11,9 +11,9 @@ export default function FeedbackInfoBox() {
|
|||
title="Feedback"
|
||||
mainContent={
|
||||
<Typography variant="subtitle2">
|
||||
Your input is crucial to us! We'd love to hear your thoughts on
|
||||
Atomic Swaps. We personally read every response to improve the
|
||||
project. Got two minutes to share?
|
||||
Your input is crucial to us! We'd love to hear your thoughts on Atomic
|
||||
Swaps. We personally read every response to improve the project. Got
|
||||
two minutes to share?
|
||||
</Typography>
|
||||
}
|
||||
additionalContent={
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
Typography,
|
||||
IconButton,
|
||||
Box,
|
||||
makeStyles,
|
||||
Tooltip,
|
||||
Select,
|
||||
MenuItem,
|
||||
|
|
@ -20,8 +19,8 @@ import {
|
|||
DialogTitle,
|
||||
useTheme,
|
||||
Switch,
|
||||
} from "@material-ui/core";
|
||||
import InfoBox from "renderer/components/modal/swap/InfoBox";
|
||||
SelectChangeEvent,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
removeNode,
|
||||
resetSettings,
|
||||
|
|
@ -36,46 +35,48 @@ import {
|
|||
Network,
|
||||
setTheme,
|
||||
} from "store/features/settingsSlice";
|
||||
import { useAppDispatch, useAppSelector, useNodes, useSettings } from "store/hooks";
|
||||
import {
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
useNodes,
|
||||
useSettings,
|
||||
} from "store/hooks";
|
||||
import ValidatedTextField from "renderer/components/other/ValidatedTextField";
|
||||
import HelpIcon from '@material-ui/icons/HelpOutline';
|
||||
import HelpIcon from "@mui/icons-material/HelpOutline";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { Theme } from "renderer/components/theme";
|
||||
import { Add, ArrowUpward, Delete, Edit, HourglassEmpty } from "@material-ui/icons";
|
||||
import {
|
||||
Add,
|
||||
ArrowUpward,
|
||||
Delete,
|
||||
Edit,
|
||||
HourglassEmpty,
|
||||
} from "@mui/icons-material";
|
||||
import { getNetwork } from "store/config";
|
||||
import { currencySymbol } from "utils/formatUtils";
|
||||
import { setTorEnabled } from "store/features/settingsSlice";
|
||||
|
||||
import InfoBox from "renderer/components/modal/swap/InfoBox";
|
||||
|
||||
const PLACEHOLDER_ELECTRUM_RPC_URL = "ssl://blockstream.info:700";
|
||||
const PLACEHOLDER_MONERO_NODE_URL = "http://xmr-node.cakewallet.com:18081";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
title: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1),
|
||||
}
|
||||
}));
|
||||
|
||||
/**
|
||||
* The settings box, containing the settings for the GUI.
|
||||
*/
|
||||
export default function SettingsBox() {
|
||||
const classes = useStyles();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<InfoBox
|
||||
title={
|
||||
<Box className={classes.title}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||
Settings
|
||||
</Box>
|
||||
}
|
||||
mainContent={
|
||||
<Typography variant="subtitle2">
|
||||
Customize the settings of the GUI.
|
||||
Some of these require a restart to take effect.
|
||||
Customize the settings of the GUI. Some of these require a restart to
|
||||
take effect.
|
||||
</Typography>
|
||||
}
|
||||
additionalContent={
|
||||
|
|
@ -93,7 +94,11 @@ export default function SettingsBox() {
|
|||
</Table>
|
||||
</TableContainer>
|
||||
{/* Reset button with a bit of spacing */}
|
||||
<Box mt={theme.spacing(0.1)} />
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
mt: theme.spacing(2),
|
||||
})}
|
||||
/>
|
||||
<ResetButton />
|
||||
</>
|
||||
}
|
||||
|
|
@ -104,7 +109,7 @@ export default function SettingsBox() {
|
|||
}
|
||||
|
||||
/**
|
||||
* A button that allows you to reset the settings.
|
||||
* A button that allows you to reset the settings.
|
||||
* Opens a modal that asks for confirmation first.
|
||||
*/
|
||||
function ResetButton() {
|
||||
|
|
@ -118,17 +123,23 @@ function ResetButton() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Button variant="outlined" onClick={() => setModalOpen(true)}>Reset Settings</Button>
|
||||
<Button variant="outlined" onClick={() => setModalOpen(true)}>
|
||||
Reset Settings
|
||||
</Button>
|
||||
<Dialog open={modalOpen} onClose={() => setModalOpen(false)}>
|
||||
<DialogTitle>Reset Settings</DialogTitle>
|
||||
<DialogContent>Are you sure you want to reset the settings?</DialogContent>
|
||||
<DialogContent>
|
||||
Are you sure you want to reset the settings?
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setModalOpen(false)}>Cancel</Button>
|
||||
<Button color="primary" onClick={onReset}>Reset</Button>
|
||||
<Button color="primary" onClick={onReset}>
|
||||
Reset
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -142,13 +153,18 @@ function FetchFiatPricesSetting() {
|
|||
<>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<SettingLabel label="Query fiat prices" tooltip="Whether to fetch fiat prices via the clearnet. This is required for the price display to work. If you require total anonymity and don't use a VPN, you should disable this." />
|
||||
<SettingLabel
|
||||
label="Query fiat prices"
|
||||
tooltip="Whether to fetch fiat prices via the clearnet. This is required for the price display to work. If you require total anonymity and don't use a VPN, you should disable this."
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Switch
|
||||
color="primary"
|
||||
checked={fetchFiatPrices}
|
||||
onChange={(event) => dispatch(setFetchFiatPrices(event.currentTarget.checked))}
|
||||
onChange={(event) =>
|
||||
dispatch(setFetchFiatPrices(event.currentTarget.checked))
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
@ -163,13 +179,16 @@ function FetchFiatPricesSetting() {
|
|||
function FiatCurrencySetting() {
|
||||
const fiatCurrency = useSettings((s) => s.fiatCurrency);
|
||||
const dispatch = useAppDispatch();
|
||||
const onChange = (e: React.ChangeEvent<{ value: unknown }>) =>
|
||||
const onChange = (e: SelectChangeEvent<FiatCurrency>) =>
|
||||
dispatch(setFiatCurrency(e.target.value as FiatCurrency));
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<SettingLabel label="Fiat currency" tooltip="This is the currency that the price display will show prices in." />
|
||||
<SettingLabel
|
||||
label="Fiat currency"
|
||||
tooltip="This is the currency that the price display will show prices in."
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Select
|
||||
|
|
@ -180,7 +199,13 @@ function FiatCurrencySetting() {
|
|||
>
|
||||
{Object.values(FiatCurrency).map((currency) => (
|
||||
<MenuItem key={currency} value={currency}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', width: '100%' }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<Box>{currency}</Box>
|
||||
<Box>{currencySymbol(currency)}</Box>
|
||||
</Box>
|
||||
|
|
@ -196,7 +221,9 @@ function FiatCurrencySetting() {
|
|||
* URL validation function, forces the URL to be in the format of "protocol://host:port/"
|
||||
*/
|
||||
function isValidUrl(url: string, allowedProtocols: string[]): boolean {
|
||||
const urlPattern = new RegExp(`^(${allowedProtocols.join("|")})://[^\\s]+:\\d+/?$`);
|
||||
const urlPattern = new RegExp(
|
||||
`^(${allowedProtocols.join("|")})://[^\\s]+:\\d+/?$`,
|
||||
);
|
||||
return urlPattern.test(url);
|
||||
}
|
||||
|
||||
|
|
@ -212,22 +239,27 @@ function ElectrumRpcUrlSetting() {
|
|||
return (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<SettingLabel label="Custom Electrum RPC URL" tooltip="This is the URL of the Electrum server that the GUI will connect to. It is used to sync Bitcoin transactions. If you leave this field empty, the GUI will choose from a list of known servers at random." />
|
||||
<SettingLabel
|
||||
label="Custom Electrum RPC URL"
|
||||
tooltip="This is the URL of the Electrum server that the GUI will connect to. It is used to sync Bitcoin transactions. If you leave this field empty, the GUI will choose from a list of known servers at random."
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IconButton
|
||||
onClick={() => setTableVisible(true)}
|
||||
>
|
||||
<IconButton onClick={() => setTableVisible(true)} size="large">
|
||||
{<Edit />}
|
||||
</IconButton>
|
||||
{tableVisible ? <NodeTableModal
|
||||
open={tableVisible}
|
||||
onClose={() => setTableVisible(false)}
|
||||
network={network}
|
||||
blockchain={Blockchain.Bitcoin}
|
||||
isValid={isValid}
|
||||
placeholder={PLACEHOLDER_ELECTRUM_RPC_URL}
|
||||
/> : <></>}
|
||||
{tableVisible ? (
|
||||
<NodeTableModal
|
||||
open={tableVisible}
|
||||
onClose={() => setTableVisible(false)}
|
||||
network={network}
|
||||
blockchain={Blockchain.Bitcoin}
|
||||
isValid={isValid}
|
||||
placeholder={PLACEHOLDER_ELECTRUM_RPC_URL}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
|
@ -236,17 +268,23 @@ function ElectrumRpcUrlSetting() {
|
|||
/**
|
||||
* A label for a setting, with a tooltip icon.
|
||||
*/
|
||||
function SettingLabel({ label, tooltip }: { label: ReactNode, tooltip: string | null }) {
|
||||
return <Box style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||||
<Box>
|
||||
{label}
|
||||
function SettingLabel({
|
||||
label,
|
||||
tooltip,
|
||||
}: {
|
||||
label: ReactNode;
|
||||
tooltip: string | null;
|
||||
}) {
|
||||
return (
|
||||
<Box style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||||
<Box>{label}</Box>
|
||||
<Tooltip title={tooltip}>
|
||||
<IconButton size="small">
|
||||
<HelpIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<Tooltip title={tooltip}>
|
||||
<IconButton size="small">
|
||||
<HelpIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -261,22 +299,27 @@ function MoneroNodeUrlSetting() {
|
|||
return (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<SettingLabel label="Custom Monero Node URL" tooltip="This is the URL of the Monero node that the GUI will connect to. Ensure the node is listening for RPC connections over HTTP. If you leave this field empty, the GUI will choose from a list of known nodes at random." />
|
||||
<SettingLabel
|
||||
label="Custom Monero Node URL"
|
||||
tooltip="This is the URL of the Monero node that the GUI will connect to. Ensure the node is listening for RPC connections over HTTP. If you leave this field empty, the GUI will choose from a list of known nodes at random."
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IconButton
|
||||
onClick={() => setTableVisible(!tableVisible)}
|
||||
>
|
||||
<IconButton onClick={() => setTableVisible(!tableVisible)} size="large">
|
||||
<Edit />
|
||||
</IconButton>
|
||||
{tableVisible ? <NodeTableModal
|
||||
open={tableVisible}
|
||||
onClose={() => setTableVisible(false)}
|
||||
network={network}
|
||||
blockchain={Blockchain.Monero}
|
||||
isValid={isValid}
|
||||
placeholder={PLACEHOLDER_MONERO_NODE_URL}
|
||||
/> : <></>}
|
||||
{tableVisible ? (
|
||||
<NodeTableModal
|
||||
open={tableVisible}
|
||||
onClose={() => setTableVisible(false)}
|
||||
network={network}
|
||||
blockchain={Blockchain.Monero}
|
||||
isValid={isValid}
|
||||
placeholder={PLACEHOLDER_MONERO_NODE_URL}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
|
@ -323,7 +366,7 @@ function NodeTableModal({
|
|||
network,
|
||||
isValid,
|
||||
placeholder,
|
||||
blockchain
|
||||
blockchain,
|
||||
}: {
|
||||
network: Network;
|
||||
blockchain: Blockchain;
|
||||
|
|
@ -337,26 +380,40 @@ function NodeTableModal({
|
|||
<DialogTitle>Available Nodes</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="subtitle2">
|
||||
When the daemon is started, it will attempt to connect to the first available {blockchain} node in this list.
|
||||
If you leave this field empty or all nodes are unavailable, it will choose from a list of known nodes at random.
|
||||
Requires a restart to take effect.
|
||||
When the daemon is started, it will attempt to connect to the first
|
||||
available {blockchain} node in this list. If you leave this field
|
||||
empty or all nodes are unavailable, it will choose from a list of
|
||||
known nodes at random. Requires a restart to take effect.
|
||||
</Typography>
|
||||
<NodeTable network={network} blockchain={blockchain} isValid={isValid} placeholder={placeholder} />
|
||||
<NodeTable
|
||||
network={network}
|
||||
blockchain={blockchain}
|
||||
isValid={isValid}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onClose} size="large">Close</Button>
|
||||
<Button onClick={onClose} size="large">
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Create a circle SVG with a given color and radius
|
||||
function Circle({ color, radius = 6 }: { color: string, radius?: number }) {
|
||||
return <span>
|
||||
<svg width={radius * 2} height={radius * 2} viewBox={`0 0 ${radius * 2} ${radius * 2}`}>
|
||||
<circle cx={radius} cy={radius} r={radius} fill={color} />
|
||||
</svg>
|
||||
</span>
|
||||
function Circle({ color, radius = 6 }: { color: string; radius?: number }) {
|
||||
return (
|
||||
<span>
|
||||
<svg
|
||||
width={radius * 2}
|
||||
height={radius * 2}
|
||||
viewBox={`0 0 ${radius * 2} ${radius * 2}`}
|
||||
>
|
||||
<circle cx={radius} cy={radius} r={radius} fill={color} />
|
||||
</svg>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -367,17 +424,27 @@ function NodeStatus({ status }: { status: boolean | undefined }) {
|
|||
|
||||
switch (status) {
|
||||
case true:
|
||||
return <Tooltip title={"This node is available and responding to RPC requests"}>
|
||||
<Circle color={theme.palette.success.dark} />
|
||||
</Tooltip>;
|
||||
return (
|
||||
<Tooltip
|
||||
title={"This node is available and responding to RPC requests"}
|
||||
>
|
||||
<Circle color={theme.palette.success.dark} />
|
||||
</Tooltip>
|
||||
);
|
||||
case false:
|
||||
return <Tooltip title={"This node is not available or not responding to RPC requests"}>
|
||||
<Circle color={theme.palette.error.dark} />
|
||||
</Tooltip>;
|
||||
return (
|
||||
<Tooltip
|
||||
title={"This node is not available or not responding to RPC requests"}
|
||||
>
|
||||
<Circle color={theme.palette.error.dark} />
|
||||
</Tooltip>
|
||||
);
|
||||
default:
|
||||
return <Tooltip title={"The status of this node is currently unknown"}>
|
||||
<HourglassEmpty />
|
||||
</Tooltip>;
|
||||
return (
|
||||
<Tooltip title={"The status of this node is currently unknown"}>
|
||||
<HourglassEmpty />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -392,10 +459,10 @@ function NodeTable({
|
|||
isValid,
|
||||
placeholder,
|
||||
}: {
|
||||
network: Network,
|
||||
blockchain: Blockchain,
|
||||
isValid: (url: string) => boolean,
|
||||
placeholder: string,
|
||||
network: Network;
|
||||
blockchain: Blockchain;
|
||||
isValid: (url: string) => boolean;
|
||||
placeholder: string;
|
||||
}) {
|
||||
const availableNodes = useSettings((s) => s.nodes[network][blockchain]);
|
||||
const currentNode = availableNodes[0];
|
||||
|
|
@ -406,7 +473,7 @@ function NodeTable({
|
|||
const onAddNewNode = () => {
|
||||
dispatch(addNode({ network, type: blockchain, node: newNode }));
|
||||
setNewNode("");
|
||||
}
|
||||
};
|
||||
|
||||
const onRemoveNode = (node: string) =>
|
||||
dispatch(removeNode({ network, type: blockchain, node }));
|
||||
|
|
@ -415,20 +482,23 @@ function NodeTable({
|
|||
dispatch(moveUpNode({ network, type: blockchain, node }));
|
||||
|
||||
const moveUpButton = (node: string) => {
|
||||
if (currentNode === node)
|
||||
return <></>;
|
||||
if (currentNode === node) return <></>;
|
||||
|
||||
return (
|
||||
<Tooltip title={"Move this node to the top of the list"}>
|
||||
<IconButton onClick={() => onMoveUpNode(node)}>
|
||||
<IconButton onClick={() => onMoveUpNode(node)} size="large">
|
||||
<ArrowUpward />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer component={Paper} style={{ marginTop: '1rem' }} elevation={0}>
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
style={{ marginTop: "1rem" }}
|
||||
elevation={0}
|
||||
>
|
||||
<Table size="small">
|
||||
{/* Table header row */}
|
||||
<TableHead>
|
||||
|
|
@ -455,10 +525,13 @@ function NodeTable({
|
|||
<Box style={{ display: "flex" }}>
|
||||
<Tooltip
|
||||
title={"Remove this node from your list"}
|
||||
children={<IconButton
|
||||
onClick={() => onRemoveNode(node)}
|
||||
children={<Delete />}
|
||||
/>}
|
||||
children={
|
||||
<IconButton
|
||||
onClick={() => onRemoveNode(node)}
|
||||
children={<Delete />}
|
||||
size="large"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{moveUpButton(node)}
|
||||
</Box>
|
||||
|
|
@ -482,7 +555,13 @@ function NodeTable({
|
|||
<TableCell></TableCell>
|
||||
<TableCell>
|
||||
<Tooltip title={"Add this node to your list"}>
|
||||
<IconButton onClick={onAddNewNode} disabled={availableNodes.includes(newNode) || newNode.length === 0}>
|
||||
<IconButton
|
||||
onClick={onAddNewNode}
|
||||
disabled={
|
||||
availableNodes.includes(newNode) || newNode.length === 0
|
||||
}
|
||||
size="large"
|
||||
>
|
||||
<Add />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
|
@ -491,24 +570,28 @@ function NodeTable({
|
|||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function TorSettings() {
|
||||
const dispatch = useAppDispatch();
|
||||
const torEnabled = useSettings((settings) => settings.enableTor)
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => dispatch(setTorEnabled(event.target.checked));
|
||||
const status = (state: boolean) => state === true ? "enabled" : "disabled";
|
||||
const torEnabled = useSettings((settings) => settings.enableTor);
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) =>
|
||||
dispatch(setTorEnabled(event.target.checked));
|
||||
const status = (state: boolean) => (state === true ? "enabled" : "disabled");
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<SettingLabel label="Use Tor" tooltip="Tor (The Onion Router) is a decentralized network allowing for anonymous browsing. If enabled, the app will use its internal Tor client to hide your IP address from the maker. Requires a restart to take effect." />
|
||||
<SettingLabel
|
||||
label="Use Tor"
|
||||
tooltip="Tor (The Onion Router) is a decentralized network allowing for anonymous browsing. If enabled, the app will use its internal Tor client to hide your IP address from the maker. Requires a restart to take effect."
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Switch checked={torEnabled} onChange={handleChange} color="primary" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Box, makeStyles } from "@material-ui/core";
|
||||
import { Box } from "@mui/material";
|
||||
import ContactInfoBox from "./ContactInfoBox";
|
||||
import DonateInfoBox from "./DonateInfoBox";
|
||||
import DaemonControlBox from "./DaemonControlBox";
|
||||
|
|
@ -7,17 +7,7 @@ import ExportDataBox from "./ExportDataBox";
|
|||
import { useLocation } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
outer: {
|
||||
display: "flex",
|
||||
gap: theme.spacing(2),
|
||||
flexDirection: "column",
|
||||
paddingBottom: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function SettingsPage() {
|
||||
const classes = useStyles();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -28,7 +18,14 @@ export default function SettingsPage() {
|
|||
}, [location]);
|
||||
|
||||
return (
|
||||
<Box className={classes.outer}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
gap: 2,
|
||||
flexDirection: "column",
|
||||
paddingBottom: 2,
|
||||
}}
|
||||
>
|
||||
<SettingsBox />
|
||||
<ExportDataBox />
|
||||
<DaemonControlBox />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Typography } from "@material-ui/core";
|
||||
import { Typography } from "@mui/material";
|
||||
import { useAppSelector } from "store/hooks";
|
||||
import SwapTxLockAlertsBox from "../../alert/SwapTxLockAlertsBox";
|
||||
import SwapDialog from "../../modal/swap/SwapDialog";
|
||||
|
|
|
|||
|
|
@ -1,35 +1,32 @@
|
|||
import { getLogsOfSwap, saveLogFiles } from 'renderer/rpc'
|
||||
import PromiseInvokeButton from 'renderer/components/PromiseInvokeButton'
|
||||
import { store } from 'renderer/store/storeRenderer'
|
||||
import { ButtonProps } from '@material-ui/core'
|
||||
import { logsToRawString } from 'utils/parseUtils'
|
||||
import { getLogsOfSwap, saveLogFiles } from "renderer/rpc";
|
||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||
import { store } from "renderer/store/storeRenderer";
|
||||
import { ButtonProps } from "@mui/material";
|
||||
import { logsToRawString } from "utils/parseUtils";
|
||||
|
||||
interface ExportLogsButtonProps extends ButtonProps {
|
||||
swap_id: string
|
||||
swap_id: string;
|
||||
}
|
||||
|
||||
export default function ExportLogsButton({ swap_id, ...buttonProps }: ExportLogsButtonProps) {
|
||||
async function handleExportLogs() {
|
||||
const swapLogs = await getLogsOfSwap(swap_id, false)
|
||||
const daemonLogs = store.getState().rpc?.logs
|
||||
export default function ExportLogsButton({
|
||||
swap_id,
|
||||
...buttonProps
|
||||
}: ExportLogsButtonProps) {
|
||||
async function handleExportLogs() {
|
||||
const swapLogs = await getLogsOfSwap(swap_id, false);
|
||||
const daemonLogs = store.getState().rpc?.logs;
|
||||
|
||||
const logContent = {
|
||||
swap_logs: logsToRawString(swapLogs.logs),
|
||||
daemon_logs: logsToRawString(daemonLogs),
|
||||
}
|
||||
const logContent = {
|
||||
swap_logs: logsToRawString(swapLogs.logs),
|
||||
daemon_logs: logsToRawString(daemonLogs),
|
||||
};
|
||||
|
||||
await saveLogFiles(
|
||||
`swap_${swap_id}_logs.zip`,
|
||||
logContent
|
||||
)
|
||||
}
|
||||
await saveLogFiles(`swap_${swap_id}_logs.zip`, logContent);
|
||||
}
|
||||
|
||||
return (
|
||||
<PromiseInvokeButton
|
||||
onInvoke={handleExportLogs}
|
||||
{...buttonProps}
|
||||
>
|
||||
Export Logs
|
||||
</PromiseInvokeButton>
|
||||
)
|
||||
return (
|
||||
<PromiseInvokeButton onInvoke={handleExportLogs} {...buttonProps}>
|
||||
Export Logs
|
||||
</PromiseInvokeButton>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,17 @@
|
|||
import {
|
||||
Box,
|
||||
Collapse,
|
||||
IconButton,
|
||||
makeStyles,
|
||||
TableCell,
|
||||
TableRow,
|
||||
} from "@material-ui/core";
|
||||
import ArrowForwardIcon from "@material-ui/icons/ArrowForward";
|
||||
import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown";
|
||||
import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp";
|
||||
import { Box, Collapse, IconButton, TableCell, TableRow } from "@mui/material";
|
||||
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
|
||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import { GetSwapInfoResponse } from "models/tauriModel";
|
||||
import { useState } from "react";
|
||||
import TruncatedText from "renderer/components/other/TruncatedText";
|
||||
import { PiconeroAmount, SatsAmount } from "../../../other/Units";
|
||||
import HistoryRowActions from "./HistoryRowActions";
|
||||
import HistoryRowExpanded from "./HistoryRowExpanded";
|
||||
import { bobStateNameToHumanReadable, GetSwapInfoResponseExt } from "models/tauriModelExt";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
amountTransferContainer: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
import {
|
||||
bobStateNameToHumanReadable,
|
||||
GetSwapInfoResponseExt,
|
||||
} from "models/tauriModelExt";
|
||||
|
||||
function AmountTransfer({
|
||||
btcAmount,
|
||||
|
|
@ -32,10 +20,14 @@ function AmountTransfer({
|
|||
xmrAmount: number;
|
||||
btcAmount: number;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Box className={classes.amountTransferContainer}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<SatsAmount amount={btcAmount} />
|
||||
<ArrowForwardIcon />
|
||||
<PiconeroAmount amount={xmrAmount} />
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { Tooltip } from "@material-ui/core";
|
||||
import { ButtonProps } from "@material-ui/core/Button/Button";
|
||||
import { green, red } from "@material-ui/core/colors";
|
||||
import DoneIcon from "@material-ui/icons/Done";
|
||||
import ErrorIcon from "@material-ui/icons/Error";
|
||||
import PlayArrowIcon from "@material-ui/icons/PlayArrow";
|
||||
import { Tooltip } from "@mui/material";
|
||||
import { ButtonProps } from "@mui/material/Button";
|
||||
import { green, red } from "@mui/material/colors";
|
||||
import DoneIcon from "@mui/icons-material/Done";
|
||||
import ErrorIcon from "@mui/icons-material/Error";
|
||||
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
|
||||
import { GetSwapInfoResponse } from "models/tauriModel";
|
||||
import {
|
||||
BobStateName,
|
||||
|
|
@ -28,7 +28,7 @@ export function SwapResumeButton({
|
|||
onInvoke={() => resumeSwap(swap.swap_id)}
|
||||
{...props}
|
||||
>
|
||||
{ children }
|
||||
{children}
|
||||
</PromiseInvokeButton>
|
||||
);
|
||||
}
|
||||
|
|
@ -79,7 +79,9 @@ export default function HistoryRowActions(swap: GetSwapInfoResponse) {
|
|||
if (swap.state_name === BobStateName.BtcPunished) {
|
||||
return (
|
||||
<Tooltip title="You have been punished. You can attempt to recover the Monero with the help of the other party but that is not guaranteed to work">
|
||||
<SwapResumeButton swap={swap} size="small">Attempt recovery</SwapResumeButton>
|
||||
<SwapResumeButton swap={swap} size="small">
|
||||
Attempt recovery
|
||||
</SwapResumeButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
import {
|
||||
Box,
|
||||
Link,
|
||||
makeStyles,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableRow,
|
||||
} from "@material-ui/core";
|
||||
import { OpenInNew } from "@material-ui/icons";
|
||||
} from "@mui/material";
|
||||
import { OpenInNew } from "@mui/icons-material";
|
||||
import { GetSwapInfoResponse } from "models/tauriModel";
|
||||
import ActionableMonospaceTextBox from "renderer/components/other/ActionableMonospaceTextBox";
|
||||
import MonospaceTextBox from "renderer/components/other/MonospaceTextBox";
|
||||
|
|
@ -23,33 +22,19 @@ import { getBitcoinTxExplorerUrl } from "utils/conversionUtils";
|
|||
import SwapLogFileOpenButton from "./SwapLogFileOpenButton";
|
||||
import ExportLogsButton from "./ExportLogsButton";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
outer: {
|
||||
display: "grid",
|
||||
padding: theme.spacing(1),
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
outerAddressBox: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
actionsOuter: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function HistoryRowExpanded({
|
||||
swap,
|
||||
}: {
|
||||
swap: GetSwapInfoResponse;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Box className={classes.outer}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
padding: 1,
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableBody>
|
||||
|
|
@ -95,7 +80,13 @@ export default function HistoryRowExpanded({
|
|||
<TableRow>
|
||||
<TableCell>Maker Address</TableCell>
|
||||
<TableCell>
|
||||
<Box className={classes.outerAddressBox}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
{swap.seller.addresses.map((addr) => (
|
||||
<ActionableMonospaceTextBox
|
||||
key={addr}
|
||||
|
|
@ -114,23 +105,30 @@ export default function HistoryRowExpanded({
|
|||
href={getBitcoinTxExplorerUrl(swap.tx_lock_id, isTestnet())}
|
||||
target="_blank"
|
||||
>
|
||||
<MonospaceTextBox>
|
||||
{swap.tx_lock_id}
|
||||
</MonospaceTextBox>
|
||||
<MonospaceTextBox>{swap.tx_lock_id}</MonospaceTextBox>
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Box className={classes.actionsOuter}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<SwapLogFileOpenButton
|
||||
swapId={swap.swap_id}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
<ExportLogsButton swap_id={swap.swap_id} variant="outlined"
|
||||
size="small"/>
|
||||
<ExportLogsButton
|
||||
swap_id={swap.swap_id}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import {
|
||||
Box,
|
||||
makeStyles,
|
||||
Paper,
|
||||
Table,
|
||||
TableBody,
|
||||
|
|
@ -8,23 +7,20 @@ import {
|
|||
TableContainer,
|
||||
TableHead,
|
||||
TableRow,
|
||||
} from "@material-ui/core";
|
||||
} from "@mui/material";
|
||||
import { useSwapInfosSortedByDate } from "../../../../../store/hooks";
|
||||
import HistoryRow from "./HistoryRow";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
outer: {
|
||||
paddingTop: theme.spacing(1),
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function HistoryTable() {
|
||||
const classes = useStyles();
|
||||
const swapSortedByDate = useSwapInfosSortedByDate();
|
||||
|
||||
return (
|
||||
<Box className={classes.outer}>
|
||||
<Box
|
||||
sx={{
|
||||
paddingTop: 1,
|
||||
paddingBottom: 1,
|
||||
}}
|
||||
>
|
||||
<TableContainer component={Paper}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import {
|
|||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
} from "@material-ui/core";
|
||||
import { ButtonProps } from "@material-ui/core/Button/Button";
|
||||
} from "@mui/material";
|
||||
import { ButtonProps } from "@mui/material/Button";
|
||||
import { CliLog, parseCliLogString } from "models/cliModel";
|
||||
import { GetLogsResponse } from "models/tauriModel";
|
||||
import { useState } from "react";
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import {
|
|||
DialogContent,
|
||||
DialogContentText,
|
||||
Link,
|
||||
} from "@material-ui/core";
|
||||
import { ButtonProps } from "@material-ui/core/Button/Button";
|
||||
} from "@mui/material";
|
||||
import { ButtonProps } from "@mui/material/Button";
|
||||
import { BobStateName, GetSwapInfoResponseExt } from "models/tauriModelExt";
|
||||
import TruncatedText from "renderer/components/other/TruncatedText";
|
||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||
|
|
@ -20,6 +20,7 @@ import {
|
|||
import { useAppDispatch, useAppSelector } from "store/hooks";
|
||||
import DialogHeader from "../../../modal/DialogHeader";
|
||||
import ScrollablePaperTextBox from "../../../other/ScrollablePaperTextBox";
|
||||
import { MoneroRecoveryResponse } from "models/rpcModel";
|
||||
|
||||
function MoneroRecoveryKeysDialog({
|
||||
swap_id,
|
||||
|
|
@ -115,8 +116,10 @@ export function SwapMoneroRecoveryButton({
|
|||
return (
|
||||
<>
|
||||
<PromiseInvokeButton
|
||||
onInvoke={() => getMoneroRecoveryKeys(swap.swap_id)}
|
||||
onSuccess={(keys) =>
|
||||
onInvoke={(): Promise<MoneroRecoveryResponse> =>
|
||||
getMoneroRecoveryKeys(swap.swap_id)
|
||||
}
|
||||
onSuccess={(keys: MoneroRecoveryResponse) =>
|
||||
store.dispatch(rpcSetMoneroRecoveryKeys([swap.swap_id, keys]))
|
||||
}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Box } from "@material-ui/core";
|
||||
import { Alert, AlertTitle } from "@material-ui/lab";
|
||||
import { Box } from "@mui/material";
|
||||
import { Alert, AlertTitle } from "@mui/material";
|
||||
import { removeAlert } from "store/features/alertsSlice";
|
||||
import { useAppDispatch, useAppSelector } from "store/hooks";
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,19 @@
|
|||
import { Box, makeStyles } from "@material-ui/core";
|
||||
import { Box } from "@mui/material";
|
||||
import ApiAlertsBox from "./ApiAlertsBox";
|
||||
import SwapWidget from "./SwapWidget";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
outer: {
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
paddingBottom: theme.spacing(1),
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function SwapPage() {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Box className={classes.outer}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
paddingBottom: 1,
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<ApiAlertsBox />
|
||||
<SwapWidget />
|
||||
</Box>
|
||||
|
|
|
|||
|
|
@ -2,15 +2,14 @@ import {
|
|||
Box,
|
||||
Fab,
|
||||
LinearProgress,
|
||||
makeStyles,
|
||||
Paper,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
import InputAdornment from "@material-ui/core/InputAdornment";
|
||||
import ArrowDownwardIcon from "@material-ui/icons/ArrowDownward";
|
||||
import SwapHorizIcon from "@material-ui/icons/SwapHoriz";
|
||||
import { Alert } from "@material-ui/lab";
|
||||
} from "@mui/material";
|
||||
import InputAdornment from "@mui/material/InputAdornment";
|
||||
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
||||
import SwapHorizIcon from "@mui/icons-material/SwapHoriz";
|
||||
import { Alert } from "@mui/material";
|
||||
import { ExtendedMakerStatus } from "models/apiModel";
|
||||
import { ChangeEvent, useEffect, useState } from "react";
|
||||
import { useAppSelector } from "store/hooks";
|
||||
|
|
@ -29,47 +28,10 @@ function isRegistryDown(reconnectionAttempts: number): boolean {
|
|||
return reconnectionAttempts > RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
inner: {
|
||||
width: "min(480px, 100%)",
|
||||
minHeight: "150px",
|
||||
display: "grid",
|
||||
padding: theme.spacing(1),
|
||||
gridGap: theme.spacing(1),
|
||||
},
|
||||
header: {
|
||||
padding: 0,
|
||||
},
|
||||
headerText: {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
makerInfo: {
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
swapIconOuter: {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
},
|
||||
swapIcon: {
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
noMakersAlertOuter: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
noMakersAlertButtonsOuter: {
|
||||
display: "flex",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
function Title() {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Box className={classes.header}>
|
||||
<Typography variant="h5" className={classes.headerText}>
|
||||
<Box sx={{ padding: 0 }}>
|
||||
<Typography variant="h5" sx={{ padding: 1 }}>
|
||||
Swap
|
||||
</Typography>
|
||||
</Box>
|
||||
|
|
@ -81,8 +43,6 @@ function HasMakerSwapWidget({
|
|||
}: {
|
||||
selectedMaker: ExtendedMakerStatus;
|
||||
}) {
|
||||
const classes = useStyles();
|
||||
|
||||
const forceShowDialog = useAppSelector((state) => state.swap.state !== null);
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [btcFieldValue, setBtcFieldValue] = useState<number | string>(
|
||||
|
|
@ -133,7 +93,16 @@ function HasMakerSwapWidget({
|
|||
// 'elevation' prop can't be passed down (type def issue)
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
<Box className={classes.inner} component={Paper} elevation={5}>
|
||||
<Paper
|
||||
variant="outlined"
|
||||
sx={(theme) => ({
|
||||
width: "min(480px, 100%)",
|
||||
minHeight: "150px",
|
||||
display: "grid",
|
||||
padding: theme.spacing(2),
|
||||
gridGap: theme.spacing(1),
|
||||
})}
|
||||
>
|
||||
<Title />
|
||||
<TextField
|
||||
label="For this many BTC"
|
||||
|
|
@ -148,7 +117,7 @@ function HasMakerSwapWidget({
|
|||
endAdornment: <InputAdornment position="end">BTC</InputAdornment>,
|
||||
}}
|
||||
/>
|
||||
<Box className={classes.swapIconOuter}>
|
||||
<Box sx={{ display: "flex", justifyContent: "center" }}>
|
||||
<ArrowDownwardIcon fontSize="small" />
|
||||
</Box>
|
||||
<TextField
|
||||
|
|
@ -162,14 +131,14 @@ function HasMakerSwapWidget({
|
|||
/>
|
||||
<MakerSelect />
|
||||
<Fab variant="extended" color="primary" onClick={handleGuideDialogOpen}>
|
||||
<SwapHorizIcon className={classes.swapIcon} />
|
||||
<SwapHorizIcon sx={{ marginRight: 1 }} />
|
||||
Swap
|
||||
</Fab>
|
||||
<SwapDialog
|
||||
open={showDialog || forceShowDialog}
|
||||
onClose={() => setShowDialog(false)}
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -178,18 +147,15 @@ function HasNoMakersSwapWidget() {
|
|||
const isPublicRegistryDown = useAppSelector((state) =>
|
||||
isRegistryDown(state.makers.registry.connectionFailsCount),
|
||||
);
|
||||
const classes = useStyles();
|
||||
|
||||
const alertBox = isPublicRegistryDown ? (
|
||||
<Alert severity="info">
|
||||
<Box className={classes.noMakersAlertOuter}>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||
<Typography>
|
||||
Currently, the public registry of makers seems to be unreachable.
|
||||
Here's what you can do:
|
||||
<ul>
|
||||
<li>
|
||||
Try discovering a maker by connecting to a rendezvous point
|
||||
</li>
|
||||
<li>Try discovering a maker by connecting to a rendezvous point</li>
|
||||
<li>
|
||||
Try again later when the public registry may be reachable again
|
||||
</li>
|
||||
|
|
@ -202,19 +168,17 @@ function HasNoMakersSwapWidget() {
|
|||
</Alert>
|
||||
) : (
|
||||
<Alert severity="info">
|
||||
<Box className={classes.noMakersAlertOuter}>
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||
<Typography>
|
||||
Currently, there are no makers (trading partners) available in the
|
||||
official registry. Here's what you can do:
|
||||
<ul>
|
||||
<li>
|
||||
Try discovering a maker by connecting to a rendezvous point
|
||||
</li>
|
||||
<li>Try discovering a maker by connecting to a rendezvous point</li>
|
||||
<li>Add a new maker to the public registry</li>
|
||||
<li>Try again later when more makers may be available</li>
|
||||
</ul>
|
||||
</Typography>
|
||||
<Box>
|
||||
<Box sx={{ display: "flex", gap: 1 }}>
|
||||
<MakerSubmitDialogOpenButton />
|
||||
<ListSellersDialogOpenButton />
|
||||
</Box>
|
||||
|
|
@ -225,19 +189,27 @@ function HasNoMakersSwapWidget() {
|
|||
return (
|
||||
<Box>
|
||||
{alertBox}
|
||||
<SwapDialog open={forceShowDialog} onClose={() => { }} />
|
||||
<SwapDialog open={forceShowDialog} onClose={() => {}} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function MakerLoadingSwapWidget() {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
// 'elevation' prop can't be passed down (type def issue)
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
<Box className={classes.inner} component={Paper} elevation={15}>
|
||||
<Box
|
||||
component={Paper}
|
||||
elevation={15}
|
||||
sx={{
|
||||
width: "min(480px, 100%)",
|
||||
minHeight: "150px",
|
||||
display: "grid",
|
||||
padding: 1,
|
||||
gridGap: 1,
|
||||
}}
|
||||
>
|
||||
<Title />
|
||||
<LinearProgress />
|
||||
</Box>
|
||||
|
|
@ -245,9 +217,7 @@ function MakerLoadingSwapWidget() {
|
|||
}
|
||||
|
||||
export default function SwapWidget() {
|
||||
const selectedMaker = useAppSelector(
|
||||
(state) => state.makers.selectedMaker,
|
||||
);
|
||||
const selectedMaker = useAppSelector((state) => state.makers.selectedMaker);
|
||||
// If we fail more than RECONNECTION_ATTEMPTS_UNTIL_ASSUME_DOWN reconnect attempts, we'll show the "no makers" widget. We can assume the public registry is down.
|
||||
const makerLoading = useAppSelector(
|
||||
(state) =>
|
||||
|
|
@ -258,8 +228,10 @@ export default function SwapWidget() {
|
|||
if (makerLoading) {
|
||||
return <MakerLoadingSwapWidget />;
|
||||
}
|
||||
if (selectedMaker) {
|
||||
return <HasMakerSwapWidget selectedMaker={selectedMaker} />;
|
||||
|
||||
if (selectedMaker === null) {
|
||||
return <HasNoMakersSwapWidget />;
|
||||
}
|
||||
return <HasNoMakersSwapWidget />;
|
||||
|
||||
return <HasMakerSwapWidget selectedMaker={selectedMaker} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,30 +1,21 @@
|
|||
import { Box, makeStyles, Typography } from "@material-ui/core";
|
||||
import { Alert } from "@material-ui/lab";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { Alert } from "@mui/material";
|
||||
import WithdrawWidget from "./WithdrawWidget";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
outer: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gridGap: theme.spacing(0.5),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function WalletPage() {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Box className={classes.outer}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "1rem",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h3">Wallet</Typography>
|
||||
<Alert severity="info">
|
||||
You do not have to deposit money before starting a swap. Instead, you
|
||||
will be greeted with a deposit address after you initiate one.
|
||||
</Alert>
|
||||
<Typography variant="subtitle1">
|
||||
If funds are left in your wallet after a swap, you can withdraw them to
|
||||
your wallet. If you decide to leave them inside the internal wallet, the
|
||||
funds will automatically be used when starting a new swap.
|
||||
</Typography>
|
||||
<WithdrawWidget />
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import RefreshIcon from "@material-ui/icons/Refresh";
|
||||
import RefreshIcon from "@mui/icons-material/Refresh";
|
||||
import PromiseInvokeButton from "renderer/components/PromiseInvokeButton";
|
||||
import { checkBitcoinBalance } from "renderer/rpc";
|
||||
import { isSyncingBitcoin } from "store/hooks";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Box, Button, makeStyles, Typography } from "@material-ui/core";
|
||||
import SendIcon from "@material-ui/icons/Send";
|
||||
import { Box, Button, Typography } from "@mui/material";
|
||||
import SendIcon from "@mui/icons-material/Send";
|
||||
import { useState } from "react";
|
||||
import { SatsAmount } from "renderer/components/other/Units";
|
||||
import { useAppSelector } from "store/hooks";
|
||||
|
|
@ -8,16 +8,7 @@ import InfoBox from "../../modal/swap/InfoBox";
|
|||
import WithdrawDialog from "../../modal/wallet/WithdrawDialog";
|
||||
import WalletRefreshButton from "./WalletRefreshButton";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
title: {
|
||||
alignItems: "center",
|
||||
display: "flex",
|
||||
gap: theme.spacing(0.5),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function WithdrawWidget() {
|
||||
const classes = useStyles();
|
||||
const walletBalance = useAppSelector((state) => state.rpc.state.balance);
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
|
||||
|
|
@ -29,7 +20,7 @@ export default function WithdrawWidget() {
|
|||
<>
|
||||
<InfoBox
|
||||
title={
|
||||
<Box className={classes.title}>
|
||||
<Box sx={{ alignItems: "center", display: "flex", gap: 0.5 }}>
|
||||
Wallet Balance
|
||||
<WalletRefreshButton />
|
||||
</Box>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue