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
+
;
+}
\ 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
+
+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 (
-
- );
+ 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 (
-
- )
-}
\ 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 (
+
+ );
+}
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(