diff --git a/Cargo.lock b/Cargo.lock index d10eb22c..02b5c028 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -328,6 +328,24 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.1", + "raw-window-handle", + "serde", + "serde_repr", + "tokio", + "url", + "zbus", +] + [[package]] name = "asn1-rs" version = "0.5.2" @@ -453,7 +471,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a" dependencies = [ "brotli 3.5.0", - "bzip2", + "bzip2 0.4.4", "flate2", "futures-core", "memchr", @@ -830,7 +848,7 @@ dependencies = [ [[package]] name = "bdk_chain" version = "0.20.0" -source = "git+https://github.com/Einliterflasche/bdk?branch=bump/rusqlite-0.32#2e57dc7495c14ed334fb525bf17f002d0a8ff6df" +source = "git+https://github.com/Einliterflasche/bdk?branch=bump%2Frusqlite-0.32#2e57dc7495c14ed334fb525bf17f002d0a8ff6df" dependencies = [ "bdk_core", "bitcoin 0.32.6", @@ -842,7 +860,7 @@ dependencies = [ [[package]] name = "bdk_core" version = "0.3.0" -source = "git+https://github.com/Einliterflasche/bdk?branch=bump/rusqlite-0.32#2e57dc7495c14ed334fb525bf17f002d0a8ff6df" +source = "git+https://github.com/Einliterflasche/bdk?branch=bump%2Frusqlite-0.32#2e57dc7495c14ed334fb525bf17f002d0a8ff6df" dependencies = [ "bitcoin 0.32.6", "hashbrown 0.14.5", @@ -852,7 +870,7 @@ dependencies = [ [[package]] name = "bdk_electrum" version = "0.19.0" -source = "git+https://github.com/Einliterflasche/bdk?branch=bump/rusqlite-0.32#2e57dc7495c14ed334fb525bf17f002d0a8ff6df" +source = "git+https://github.com/Einliterflasche/bdk?branch=bump%2Frusqlite-0.32#2e57dc7495c14ed334fb525bf17f002d0a8ff6df" dependencies = [ "bdk_core", "electrum-client 0.22.0", @@ -861,7 +879,7 @@ dependencies = [ [[package]] name = "bdk_wallet" version = "1.0.0-beta.5" -source = "git+https://github.com/Einliterflasche/bdk?branch=bump/rusqlite-0.32#2e57dc7495c14ed334fb525bf17f002d0a8ff6df" +source = "git+https://github.com/Einliterflasche/bdk?branch=bump%2Frusqlite-0.32#2e57dc7495c14ed334fb525bf17f002d0a8ff6df" dependencies = [ "bdk_chain", "bitcoin 0.32.6", @@ -1329,6 +1347,15 @@ dependencies = [ "libc", ] +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + [[package]] name = "bzip2-sys" version = "0.1.13+1.0.8" @@ -1674,6 +1701,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "convert_case" version = "0.4.0" @@ -2134,6 +2167,12 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + [[package]] name = "der" version = "0.7.10" @@ -2454,6 +2493,18 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dispatch2" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a0d569e003ff27784e0e14e4a594048698e0c0f0b66cabcb51511be55a7caa0" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.1", + "libc", + "objc2 0.6.1", +] + [[package]] name = "dispatch2" version = "0.3.0" @@ -2950,6 +3001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -4729,6 +4781,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "liblzma" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66352d7a8ac12d4877b6e6ea5a9b7650ee094257dc40889955bea5bc5b08c1d0" +dependencies = [ + "liblzma-sys", +] + +[[package]] +name = "liblzma-sys" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5839bad90c3cc2e0b8c4ed8296b80e86040240f81d46b9c0e9bc8dd51ddd3af1" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "libm" version = "0.2.15" @@ -5264,6 +5336,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a" +dependencies = [ + "zlib-rs", +] + [[package]] name = "libz-sys" version = "1.1.22" @@ -6071,7 +6152,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ "bitflags 2.9.1", - "dispatch2", + "dispatch2 0.3.0", "objc2 0.6.1", ] @@ -6082,7 +6163,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" dependencies = [ "bitflags 2.9.1", - "dispatch2", + "dispatch2 0.3.0", "objc2 0.6.1", "objc2-core-foundation", "objc2-io-surface", @@ -6482,6 +6563,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac", +] + [[package]] name = "pem" version = "3.0.5" @@ -7527,6 +7618,31 @@ dependencies = [ "subtle", ] +[[package]] +name = "rfd" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80c844748fdc82aae252ee4594a89b6e7ebef1063de7951545564cbc4e57075d" +dependencies = [ + "ashpd", + "block2 0.6.1", + "dispatch2 0.2.0", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.1", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "ring" version = "0.16.20" @@ -9568,6 +9684,46 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "tauri-plugin-dialog" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33318fe222fc2a612961de8b0419e2982767f213f54a4d3a21b0d7b85c41df8" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.12", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ead0daec5d305adcefe05af9d970fc437bcc7996052d564e7393eb291252da" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.12", + "toml", + "url", +] + [[package]] name = "tauri-plugin-opener" version = "2.2.6" @@ -9993,6 +10149,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", + "tracing", "windows-sys 0.52.0", ] @@ -11596,6 +11753,7 @@ dependencies = [ "tauri-build", "tauri-plugin-cli", "tauri-plugin-clipboard-manager", + "tauri-plugin-dialog", "tauri-plugin-opener", "tauri-plugin-process", "tauri-plugin-shell", @@ -11604,6 +11762,7 @@ dependencies = [ "tauri-plugin-updater", "tracing", "uuid", + "zip 4.0.0", ] [[package]] @@ -13117,6 +13276,7 @@ dependencies = [ "ordered-stream", "serde", "serde_repr", + "tokio", "tracing", "uds_windows", "windows-sys 0.59.0", @@ -13254,7 +13414,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" dependencies = [ "byteorder", - "bzip2", + "bzip2 0.4.4", "crc32fast", "flate2", "thiserror 1.0.69", @@ -13276,6 +13436,78 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "zip" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "153a6fff49d264c4babdcfa6b4d534747f520e56e8f0f384f3b808c4b64cc1fd" +dependencies = [ + "aes", + "arbitrary", + "bzip2 0.5.2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.3.3", + "hmac", + "indexmap 2.9.0", + "liblzma", + "memchr", + "pbkdf2", + "sha1", + "time 0.3.41", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8" + +[[package]] +name = "zopfli" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.15+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zvariant" version = "5.5.3" @@ -13285,6 +13517,7 @@ dependencies = [ "endi", "enumflags2", "serde", + "url", "winnow 0.7.10", "zvariant_derive", "zvariant_utils", diff --git a/docs/components/Logo.tsx b/docs/components/Logo.tsx new file mode 100644 index 00000000..20597066 --- /dev/null +++ b/docs/components/Logo.tsx @@ -0,0 +1,8 @@ +import Image from 'next/image'; + +export default function Logo() { + return
+ UnstoppableSwap + UnstoppableSwap +
; +} \ No newline at end of file diff --git a/docs/pages/_meta.json b/docs/pages/_meta.json index 7e189126..141676f5 100644 --- a/docs/pages/_meta.json +++ b/docs/pages/_meta.json @@ -4,5 +4,6 @@ "usage": "Usage", "advanced": "Advanced", "becoming_a_maker": "Becoming a Maker", + "send_feedback": "Send Feedback", "donate": "Donate" } \ No newline at end of file diff --git a/docs/pages/send_feedback.mdx b/docs/pages/send_feedback.mdx new file mode 100644 index 00000000..4a208084 --- /dev/null +++ b/docs/pages/send_feedback.mdx @@ -0,0 +1,45 @@ +import Image from 'next/image' + +# Send Feedback + +We value your feedback and are committed to providing the best support possible. There are two ways to send feedback: + +## In-App Feedback + +The easiest way to send feedback is through the app: + +1. Go to the feedback tab in the sidebar or use the feedback button within a modal + Feedback Button in the swap modal +2. Write your message in the text field +3. **Important**: If you're reporting an issue, please: + - Select the relevant swap from the dropdown menu + - Check "Attach logs from the current session" + - Review the logs before sending (you can click the eye icon to view them) + - Use the redaction option if you want to remove sensitive information + +Your feedback will be answered directly in the app under the Feedback tab. + +## Email Support + +If you prefer to send an email, please include: + +1. A clear description of your issue or feedback +2. Swap logs you want to share or encountered problems with (You can use the "Export Logs" functionality in the history tab) + +Send your email to: [help@unstoppableswap.net](mailto:help@unstoppableswap.net) + +## Tips for Effective Feedback + +To help us assist you better: + +- Be specific about what you were trying to do +- Include steps to reproduce the issue +- Mention your operating system and app version +- Attach relevant logs whenever possible + +## What to Expect + +- We try to answer within 24 hours, but please be patient if you don't receive a message directly. +- You can continue the conversation through the app or email +- We may ask for additional information to help resolve your issue + diff --git a/docs/public/android-chrome-192x192.png b/docs/public/android-chrome-192x192.png new file mode 100644 index 00000000..9925f676 Binary files /dev/null and b/docs/public/android-chrome-192x192.png differ diff --git a/docs/public/android-chrome-512x512.png b/docs/public/android-chrome-512x512.png new file mode 100644 index 00000000..54d0927a Binary files /dev/null and b/docs/public/android-chrome-512x512.png differ diff --git a/docs/public/apple-touch-icon.png b/docs/public/apple-touch-icon.png new file mode 100644 index 00000000..2e72df02 Binary files /dev/null and b/docs/public/apple-touch-icon.png differ diff --git a/docs/public/favicon.ico b/docs/public/favicon.ico index 4965832f..864d48a7 100644 Binary files a/docs/public/favicon.ico and b/docs/public/favicon.ico differ diff --git a/docs/public/feedback_button_modal.png b/docs/public/feedback_button_modal.png new file mode 100644 index 00000000..11d52e9b Binary files /dev/null and b/docs/public/feedback_button_modal.png differ diff --git a/docs/public/icon.svg b/docs/public/icon.svg new file mode 100644 index 00000000..9308c0c6 --- /dev/null +++ b/docs/public/icon.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/public/manifest.webmanifest b/docs/public/manifest.webmanifest new file mode 100644 index 00000000..19e8c3f4 --- /dev/null +++ b/docs/public/manifest.webmanifest @@ -0,0 +1,14 @@ +{ + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/docs/theme.config.jsx b/docs/theme.config.jsx index d2ba8fee..ae4cdcc4 100644 --- a/docs/theme.config.jsx +++ b/docs/theme.config.jsx @@ -1,8 +1,21 @@ +import Logo from "./components/Logo"; + export default { - logo: UnstoppableSwap, + logo: , project: { link: "https://github.com/UnstoppableSwap/core", }, + head: ( + <> + UnstoppableSwap Docs + + + + + + + + ), primaryHue: 14.3, primarySaturation: 90.68, }; diff --git a/src-gui/src/renderer/components/modal/feedback/FeedbackDialog.tsx b/src-gui/src/renderer/components/modal/feedback/FeedbackDialog.tsx index 0d5852e1..bb01851a 100644 --- a/src-gui/src/renderer/components/modal/feedback/FeedbackDialog.tsx +++ b/src-gui/src/renderer/components/modal/feedback/FeedbackDialog.tsx @@ -1,356 +1,247 @@ import { - Box, - Button, - Checkbox, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - FormControlLabel, - IconButton, - MenuItem, - Paper, - Select, - Switch, - TextField, - Tooltip, - Typography, -} from "@material-ui/core"; -import { useSnackbar } from "notistack"; -import { useEffect, useState } from "react"; -import TruncatedText from "renderer/components/other/TruncatedText"; -import { store } from "renderer/store/storeRenderer"; -import { useActiveSwapInfo, useAppSelector } from "store/hooks"; -import { logsToRawString, parseDateString } from "utils/parseUtils"; -import { submitFeedbackViaHttp, AttachmentInput } from "../../../api"; -import LoadingButton from "../../other/LoadingButton"; -import { PiconeroAmount } from "../../other/Units"; -import { getLogsOfSwap, redactLogs } from "renderer/rpc"; -import logger from "utils/logger"; -import { Label, Visibility } from "@material-ui/icons"; -import CliLogsBox from "renderer/components/other/RenderedCliLog"; -import { CliLog, parseCliLogString } from "models/cliModel"; -import { addFeedbackId } from "store/features/conversationsSlice"; - -async function submitFeedback(body: string, swapId: string | null, swapLogs: string | null, daemonLogs: string | null) { - const attachments: AttachmentInput[] = []; - - if (swapId !== null) { - const swapInfo = store.getState().rpc.state.swapInfos[swapId]; - if (swapInfo) { - // Add swap info as an attachment - attachments.push({ - key: `swap_info_${swapId}.json`, - content: JSON.stringify(swapInfo, null, 2), // Pretty print JSON - }); - // Retrieve and add logs for the specific swap - try { - const logs = await getLogsOfSwap(swapId, false); - attachments.push({ - key: `swap_logs_${swapId}.txt`, - content: logs.logs.map((l) => JSON.stringify(l)).join("\n"), - }); - } catch (logError) { - logger.error(logError, "Failed to get logs for swap", { swapId }); - // Optionally add an attachment indicating log retrieval failure - attachments.push({ key: `swap_logs_${swapId}.error`, content: "Failed to retrieve swap logs." }); - } - } else { - logger.warn("Selected swap info not found in state", { swapId }); - attachments.push({ key: `swap_info_${swapId}.error`, content: "Swap info not found." }); - } - - // Add swap logs as an attachment - if (swapLogs) { - attachments.push({ - key: `swap_logs_${swapId}.txt`, - content: swapLogs, - }); - } - } - - // Handle daemon logs - if (daemonLogs !== null) { - attachments.push({ - key: "daemon_logs.txt", - content: daemonLogs, - }); - } - - // Call the updated API function - const feedbackId = await submitFeedbackViaHttp(body, attachments); - - // Dispatch only the ID - store.dispatch(addFeedbackId(feedbackId)); -} - -/* - * This component is a dialog that allows the user to submit feedback to the - * developers. The user can enter a message and optionally attach logs from a - * specific swap. - * selectedSwap = null means no swap is attached - */ -function SwapSelectDropDown({ - selectedSwap, - setSelectedSwap, -}: { - selectedSwap: string | null; - setSelectedSwap: (swapId: string | null) => void; -}) { - const swaps = useAppSelector((state) => - Object.values(state.rpc.state.swapInfos), - ); - - return ( - - ); -} - -const MAX_FEEDBACK_LENGTH = 4000; + Box, + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + IconButton, + Paper, + TextField, + Tooltip, + Typography, +} from '@material-ui/core' +import { ErrorOutline, Visibility } from '@material-ui/icons' +import ExternalLink from 'renderer/components/other/ExternalLink' +import SwapSelectDropDown from './SwapSelectDropDown' +import LogViewer from './LogViewer' +import { useFeedback, MAX_FEEDBACK_LENGTH } from './useFeedback' +import { useState } from 'react' +import PromiseInvokeButton from 'renderer/components/PromiseInvokeButton' export default function FeedbackDialog({ - open, - onClose, + open, + onClose, }: { - open: boolean; - onClose: () => void; + open: boolean + onClose: () => void }) { - const [pending, setPending] = useState(false); - const [bodyText, setBodyText] = useState(""); - const currentSwapId = useActiveSwapInfo(); + const [swapLogsEditorOpen, setSwapLogsEditorOpen] = useState(false) + const [daemonLogsEditorOpen, setDaemonLogsEditorOpen] = useState(false) - const { enqueueSnackbar } = useSnackbar(); + const { input, setInputState, logs, error, clearState, submitFeedback } = + useFeedback() - const [selectedSwap, setSelectedSwap] = useState< - string | null - >(currentSwapId?.swap_id || null); - const [swapLogs, setSwapLogs] = useState<(string | CliLog)[] | null>(null); - const [attachDaemonLogs, setAttachDaemonLogs] = useState(true); - - const [daemonLogs, setDaemonLogs] = useState<(string | CliLog)[] | null>(null); - - useEffect(() => { - // Reset logs if no swap is selected - if (selectedSwap === null) { - setSwapLogs(null); - return; + const handleClose = () => { + clearState() + onClose() } - // Fetch the logs from the rust backend and update the state - getLogsOfSwap(selectedSwap, false).then((response) => setSwapLogs(response.logs.map(parseCliLogString))) - }, [selectedSwap]); + const bodyTooLong = input.bodyText.length > MAX_FEEDBACK_LENGTH - useEffect(() => { - if (attachDaemonLogs === false) { - setDaemonLogs(null); - return; - } - - setDaemonLogs(store.getState().rpc?.logs) - }, [attachDaemonLogs]); - - // Whether to display the log editor - const [swapLogsEditorOpen, setSwapLogsEditorOpen] = useState(false); - const [daemonLogsEditorOpen, setDaemonLogsEditorOpen] = useState(false); - - const bodyTooLong = bodyText.length > MAX_FEEDBACK_LENGTH; - - const clearState = () => { - setBodyText(""); - setAttachDaemonLogs(false); - setSelectedSwap(null); - } - - const sendFeedback = async () => { - if (pending) { - return; - } - - try { - setPending(true); - await submitFeedback( - bodyText, - selectedSwap, - logsToRawString(swapLogs ?? []), - logsToRawString(daemonLogs ?? []) - ); - enqueueSnackbar("Feedback submitted successfully!", { - variant: "success", - }); - clearState() - } catch (e) { - logger.error(`Failed to submit feedback: ${e}`); - enqueueSnackbar(`Failed to submit feedback (${e})`, { - variant: "error", - }); - } finally { - setPending(false); - } - onClose(); - } - - const setSwapLogsRedacted = async (redact: boolean) => { - setSwapLogs((await getLogsOfSwap(selectedSwap, redact)).logs.map(parseCliLogString)) - } - - const setDaemonLogsRedacted = async (redact: boolean) => { - if (!redact) - return setDaemonLogs(store.getState().rpc?.logs) - - const redactedLogs = await redactLogs(daemonLogs); - setDaemonLogs(redactedLogs) - } - - return ( - - Submit Feedback - -
    -
  • Got something to say? Drop us a message below.
  • -
  • If you had an issue with a specific swap, select it from the dropdown to attach the logs. - It will help us figure out what went wrong. -
  • -
  • We appreciate you taking the time to share your thoughts! Every message is read by a core developer!
  • -
- - setBodyText(e.target.value)} - label={ - bodyTooLong - ? `Text is too long (${bodyText.length}/${MAX_FEEDBACK_LENGTH})` - : "Message" - } - multiline - minRows={4} - maxRows={4} - fullWidth - error={bodyTooLong} - /> - - - - - - setSwapLogsEditorOpen(true)} disabled={selectedSwap === null}> - - - - - - - - - + + Submit Feedback + + + + {error && ( + + + + {error} + + + )} + + + Have a question or need assistance? Message us below + or{' '} + + email us + + ! + + + setInputState((prev) => ({ + ...prev, + bodyText: e.target.value, + })) + } + label={ + bodyTooLong + ? `Text is too long (${input.bodyText.length}/${MAX_FEEDBACK_LENGTH})` + : 'Message' + } + multiline + minRows={4} + maxRows={4} + fullWidth + error={bodyTooLong} + /> + + + + Attach logs with your feedback for better support. + + + + setInputState((prev) => ({ + ...prev, + selectedSwap: swapId, + })) + } + /> + + + + setSwapLogsEditorOpen(true) + } + disabled={input.selectedSwap === null} + > + + + + + + + setInputState((prev) => ({ + ...prev, + isSwapLogsRedacted: redact, + })) + } + isRedacted={input.isSwapLogsRedacted} + /> + + + + setInputState((prev) => ({ + ...prev, + attachDaemonLogs: + e.target.checked, + })) + } + /> + } + label="Attach logs from the current session" + /> + + + + + setDaemonLogsEditorOpen(true) + } + disabled={ + input.attachDaemonLogs === false + } + > + + + + + + + + Your feedback will be answered in the app and can be + found in the Feedback tab + + + setInputState((prev) => ({ + ...prev, + isDaemonLogsRedacted: redact, + })) + } + isRedacted={input.isDaemonLogsRedacted} + /> + + + + + setAttachDaemonLogs(e.target.checked)} - /> - } - label="Attach logs from the current session" - /> - - - - setDaemonLogsEditorOpen(true)} disabled={attachDaemonLogs === false}> - - - - - - - -
- - - - Submit - - -
- ); + variant="contained" + onInvoke={submitFeedback} + onSuccess={handleClose} + > + Submit + + + + ) } - -function LogViewer( - { open, - setOpen, - logs, - redact - }: { - open: boolean, - setOpen: (_: boolean) => void, - logs: (string | CliLog)[] | null, - redact: (_: boolean) => void - }) { - return ( - setOpen(false)} fullWidth> - - - - - - These are the logs that would be attached to your feedback message and provided to us developers. - They help us narrow down the problem you encountered. - - - - - - - - Redact - redact(checked)} /> - } /> - - - - - - - ) -} \ No newline at end of file diff --git a/src-gui/src/renderer/components/modal/feedback/LogViewer.tsx b/src-gui/src/renderer/components/modal/feedback/LogViewer.tsx new file mode 100644 index 00000000..6665c754 --- /dev/null +++ b/src-gui/src/renderer/components/modal/feedback/LogViewer.tsx @@ -0,0 +1,66 @@ +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + Paper, + Switch, + Typography, +} from "@material-ui/core"; +import { CliLog } from "models/cliModel"; +import CliLogsBox from "renderer/components/other/RenderedCliLog"; + +interface LogViewerProps { + open: boolean; + setOpen: (_: boolean) => void; + logs: (string | CliLog)[] | null; + setIsRedacted: (_: boolean) => void; + isRedacted: boolean; +} + +export default function LogViewer({ + open, + setOpen, + logs, + setIsRedacted, + isRedacted +}: LogViewerProps) { + return ( + setOpen(false)} fullWidth> + + + + + + These are the logs that would be attached to your feedback message and provided to us developers. + They help us narrow down the problem you encountered. + + + + + + Redact + setIsRedacted(checked)} + /> + + } + /> + + + + + + + ); +} diff --git a/src-gui/src/renderer/components/modal/feedback/SwapSelectDropDown.tsx b/src-gui/src/renderer/components/modal/feedback/SwapSelectDropDown.tsx new file mode 100644 index 00000000..d34741ba --- /dev/null +++ b/src-gui/src/renderer/components/modal/feedback/SwapSelectDropDown.tsx @@ -0,0 +1,45 @@ +import { MenuItem, Select, Box } from "@material-ui/core"; +import TruncatedText from "renderer/components/other/TruncatedText"; +import { PiconeroAmount } from "../../other/Units"; +import { parseDateString } from "utils/parseUtils"; +import { useEffect } from "react"; +import { useSwapInfosSortedByDate } from "store/hooks"; + +interface SwapSelectDropDownProps { + selectedSwap: string | null; + setSelectedSwap: (swapId: string | null) => void; +} + +export default function SwapSelectDropDown({ + selectedSwap, + setSelectedSwap, +}: SwapSelectDropDownProps) { + const swaps = useSwapInfosSortedByDate(); + + useEffect(() => { + if (swaps.length > 0) { + setSelectedSwap(swaps[0].swap_id); + } + }, []); + + return ( + + ); +} \ No newline at end of file diff --git a/src-gui/src/renderer/components/modal/feedback/useFeedback.ts b/src-gui/src/renderer/components/modal/feedback/useFeedback.ts new file mode 100644 index 00000000..7c5d17dd --- /dev/null +++ b/src-gui/src/renderer/components/modal/feedback/useFeedback.ts @@ -0,0 +1,163 @@ +import { useState, useEffect } from 'react' +import { store } from 'renderer/store/storeRenderer' +import { useActiveSwapInfo } from 'store/hooks' +import { logsToRawString } from 'utils/parseUtils' +import { getLogsOfSwap, redactLogs } from 'renderer/rpc' +import { CliLog, parseCliLogString } from 'models/cliModel' +import logger from 'utils/logger' +import { submitFeedbackViaHttp } from 'renderer/api' +import { addFeedbackId } from 'store/features/conversationsSlice' +import { AttachmentInput } from 'models/apiModel' +import { useSnackbar } from 'notistack' + +export const MAX_FEEDBACK_LENGTH = 4000 + +interface FeedbackInputState { + bodyText: string + selectedSwap: string | null + attachDaemonLogs: boolean + isSwapLogsRedacted: boolean + isDaemonLogsRedacted: boolean +} + +interface FeedbackLogsState { + swapLogs: (string | CliLog)[] | null + daemonLogs: (string | CliLog)[] | null +} + +const initialInputState: FeedbackInputState = { + bodyText: '', + selectedSwap: null, + attachDaemonLogs: true, + isSwapLogsRedacted: false, + isDaemonLogsRedacted: false, +} + +const initialLogsState: FeedbackLogsState = { + swapLogs: null, + daemonLogs: null, +} + +export function useFeedback() { + const currentSwapId = useActiveSwapInfo() + const { enqueueSnackbar } = useSnackbar() + + const [inputState, setInputState] = useState({ + ...initialInputState, + selectedSwap: currentSwapId?.swap_id || null, + }) + const [logsState, setLogsState] = + useState(initialLogsState) + const [isPending, setIsPending] = useState(false) + const [error, setError] = useState(null) + + const bodyTooLong = inputState.bodyText.length > MAX_FEEDBACK_LENGTH + + useEffect(() => { + if (inputState.selectedSwap === null) { + setLogsState((prev) => ({ ...prev, swapLogs: null })) + return + } + + getLogsOfSwap(inputState.selectedSwap, inputState.isSwapLogsRedacted) + .then((response) => { + setLogsState((prev) => ({ + ...prev, + swapLogs: response.logs.map(parseCliLogString), + })) + setError(null) + }) + .catch((e) => { + logger.error(`Failed to fetch swap logs: ${e}`) + setLogsState((prev) => ({ ...prev, swapLogs: null })) + setError(`Failed to fetch swap logs: ${e}`) + }) + }, [inputState.selectedSwap, inputState.isSwapLogsRedacted]) + + useEffect(() => { + if (!inputState.attachDaemonLogs) { + setLogsState((prev) => ({ ...prev, daemonLogs: null })) + return + } + + try { + if (inputState.isDaemonLogsRedacted) { + redactLogs(store.getState().rpc?.logs) + .then((redactedLogs) => { + setLogsState((prev) => ({ + ...prev, + daemonLogs: redactedLogs, + })) + setError(null) + }) + .catch((e) => { + logger.error(`Failed to redact daemon logs: ${e}`) + setLogsState((prev) => ({ ...prev, daemonLogs: null })) + setError(`Failed to redact daemon logs: ${e}`) + }) + } else { + setLogsState((prev) => ({ + ...prev, + daemonLogs: store.getState().rpc?.logs, + })) + setError(null) + } + } catch (e) { + logger.error(`Failed to fetch daemon logs: ${e}`) + setLogsState((prev) => ({ ...prev, daemonLogs: null })) + setError(`Failed to fetch daemon logs: ${e}`) + } + }, [inputState.attachDaemonLogs, inputState.isDaemonLogsRedacted]) + + const clearState = () => { + setInputState(initialInputState) + setLogsState(initialLogsState) + setError(null) + } + + const submitFeedback = async () => { + if (inputState.bodyText.length === 0) { + setError('Please enter a message') + throw new Error('User did not enter a message') + } + + const attachments: AttachmentInput[] = [] + // Add swap logs as an attachment + if (logsState.swapLogs) { + attachments.push({ + key: `swap_logs_${inputState.selectedSwap}.txt`, + content: logsToRawString(logsState.swapLogs), + }) + } + + // Handle daemon logs + if (logsState.daemonLogs) { + attachments.push({ + key: 'daemon_logs.txt', + content: logsToRawString(logsState.daemonLogs), + }) + } + + // Call the updated API function + const feedbackId = await submitFeedbackViaHttp( + inputState.bodyText, + attachments + ) + + enqueueSnackbar('Feedback submitted successfully', { + variant: 'success', + }) + + // Dispatch only the ID + store.dispatch(addFeedbackId(feedbackId)) + } + + return { + input: inputState, + setInputState, + logs: logsState, + error, + clearState, + submitFeedback, + } +} diff --git a/src-gui/src/renderer/components/pages/history/table/ExportLogsButton.tsx b/src-gui/src/renderer/components/pages/history/table/ExportLogsButton.tsx new file mode 100644 index 00000000..1932d095 --- /dev/null +++ b/src-gui/src/renderer/components/pages/history/table/ExportLogsButton.tsx @@ -0,0 +1,35 @@ +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' + +interface ExportLogsButtonProps extends ButtonProps { + 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 + + const logContent = { + swap_logs: logsToRawString(swapLogs.logs), + daemon_logs: logsToRawString(daemonLogs), + } + + await saveLogFiles( + `swap_${swap_id}_logs.zip`, + logContent + ) + } + + return ( + + Export Logs + + ) +} diff --git a/src-gui/src/renderer/components/pages/history/table/HistoryRowExpanded.tsx b/src-gui/src/renderer/components/pages/history/table/HistoryRowExpanded.tsx index f60589f7..b20455f7 100644 --- a/src-gui/src/renderer/components/pages/history/table/HistoryRowExpanded.tsx +++ b/src-gui/src/renderer/components/pages/history/table/HistoryRowExpanded.tsx @@ -21,6 +21,7 @@ import { import { isTestnet } from "store/config"; import { getBitcoinTxExplorerUrl } from "utils/conversionUtils"; import SwapLogFileOpenButton from "./SwapLogFileOpenButton"; +import ExportLogsButton from "./ExportLogsButton"; const useStyles = makeStyles((theme) => ({ outer: { @@ -128,6 +129,8 @@ export default function HistoryRowExpanded({ variant="outlined" size="small" /> + ); diff --git a/src-gui/src/renderer/rpc.ts b/src-gui/src/renderer/rpc.ts index 830df87a..2716906e 100644 --- a/src-gui/src/renderer/rpc.ts +++ b/src-gui/src/renderer/rpc.ts @@ -313,3 +313,7 @@ export async function getDataDir(): Promise { export async function resolveApproval(requestId: string, accept: boolean): Promise { await invoke("resolve_approval_request", { request_id: requestId, accept }); } + +export async function saveLogFiles(zipFileName: string, content: Record): Promise { + await invokeUnsafe("save_txt_files", { zipFileName, content }); +} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5e4589eb..036022a1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -23,6 +23,7 @@ swap = { path = "../swap", features = [ "tauri" ] } sysinfo = "=0.32.1" tauri = { version = "^2.0.0", features = [ "config-json5" ] } tauri-plugin-clipboard-manager = "^2.0.0" +tauri-plugin-dialog = "2.2.2" tauri-plugin-opener = "^2.0.0" tauri-plugin-process = "^2.0.0" tauri-plugin-shell = "^2.0.0" @@ -30,6 +31,7 @@ tauri-plugin-store = "^2.0.0" tauri-plugin-updater = "^2.0.0" tracing = "0.1" uuid = "1.16.0" +zip = "4.0.0" [target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] tauri-plugin-cli = "^2.0.0" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0f6b8853..f474ac65 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,4 +1,6 @@ use anyhow::Context as AnyhowContext; +use std::collections::HashMap; +use std::io::Write; use std::result::Result; use std::sync::Arc; use swap::cli::{ @@ -18,6 +20,8 @@ use swap::cli::{ command::{Bitcoin, Monero}, }; use tauri::{async_runtime::RwLock, Manager, RunEvent}; +use tauri_plugin_dialog::DialogExt; +use zip::{write::SimpleFileOptions, ZipWriter}; /// Trait to convert Result to Result /// Tauri commands require the error type to be a string @@ -165,6 +169,7 @@ pub fn run() { .plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_dialog::init()) .invoke_handler(tauri::generate_handler![ get_balance, get_monero_addresses, @@ -186,7 +191,8 @@ pub fn run() { get_wallet_descriptor, get_data_dir, resolve_approval_request, - redact + redact, + save_txt_files ]) .setup(setup) .build(tauri::generate_context!()) @@ -275,6 +281,51 @@ async fn get_data_dir( .to_string()) } +#[tauri::command] +async fn save_txt_files( + app: tauri::AppHandle, + zip_file_name: String, + content: HashMap, +) -> Result<(), String> { + // Step 1: Get the owned PathBuf from the dialog + let path_buf_from_dialog: tauri_plugin_dialog::FilePath = app + .dialog() + .file() + .set_file_name(format!("{}.zip", &zip_file_name).as_str()) + .add_filter(&zip_file_name, &["zip"]) + .blocking_save_file() // This returns Option + .ok_or_else(|| "Dialog cancelled or file path not selected".to_string())?; // Converts to Result and unwraps to PathBuf + + // Step 2: Now get a &Path reference from the owned PathBuf. + // The user's code structure implied an .as_path().ok_or_else(...) chain which was incorrect for &Path. + // We'll directly use the PathBuf, or if &Path is strictly needed: + let selected_file_path: &std::path::Path = path_buf_from_dialog + .as_path() + .ok_or_else(|| "Could not convert file path".to_string())?; + + let zip_file = std::fs::File::create(selected_file_path) + .map_err(|e| format!("Failed to create file: {}", e))?; + + let mut zip = ZipWriter::new(zip_file); + + for (filename, file_content_str) in content.iter() { + zip.start_file( + format!("{}.txt", filename).as_str(), + SimpleFileOptions::default(), + ) // Pass &str to start_file + .map_err(|e| format!("Failed to start file {}: {}", &filename, e))?; // Use &filename + + zip.write_all(file_content_str.as_bytes()) + .map_err(|e| format!("Failed to write to file {}: {}", &filename, e))?; + // Use &filename + } + + zip.finish() + .map_err(|e| format!("Failed to finish zip: {}", e))?; + + Ok(()) +} + /// Tauri command to initialize the Context #[tauri::command] async fn initialize_context(