feat(gui): Feedback quick fixes (#331)
* refactor(gui): Simplify FeedbackDialog component and enhance feedback submission process - Consolidated state management for feedback input using a custom hook. - Improved user interface for feedback submission by including clearer instructions - Removed redundant code and improved overall component structure for better maintainability. * refactor(gui): Enhance FeedbackDialog layout and add mail link * feat(gui): Add error handling in feedback submission * feat(docs): Add brand identity to docs * feat(docs): Add Send Feedback page * feat(tauri): build base for log export feature * feat(tauri): update save_txt_files to use HashMap for file content * feat(gui): Implement log export functionality * fix(gui): adjust feedback dialog link to show docs page with instructions for mail feedback * fix(gui): minor style adjustments to export logs button * feat(gui, tauri): enhance log export functionality to include zip file naming * fix(docs): clarify docs section about exporting logs * feat(gui): initialize selected swap in SwapSelectDropDown with most recent swap * fix(gui): parse logs correctly for saving to log file * fix(gui): ensure to use the most recent swap info by using a specialized hook * fmr --------- Co-authored-by: Binarybaron <binarybaron@protonmail.com>
251
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
8
docs/components/Logo.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import Image from 'next/image';
|
||||
|
||||
export default function Logo() {
|
||||
return <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Image src="/favicon.svg" alt="UnstoppableSwap" width={32} height={32} style={{ borderRadius: '20%' }}/>
|
||||
<span>UnstoppableSwap</span>
|
||||
</div>;
|
||||
}
|
||||
|
|
@ -4,5 +4,6 @@
|
|||
"usage": "Usage",
|
||||
"advanced": "Advanced",
|
||||
"becoming_a_maker": "Becoming a Maker",
|
||||
"send_feedback": "Send Feedback",
|
||||
"donate": "Donate"
|
||||
}
|
||||
45
docs/pages/send_feedback.mdx
Normal file
|
|
@ -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
|
||||
<Image src='/feedback_button_modal.png' width={600} height={300} alt="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
|
||||
|
||||
BIN
docs/public/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
docs/public/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 8.1 KiB |
BIN
docs/public/feedback_button_modal.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
114
docs/public/icon.svg
Normal file
|
After Width: | Height: | Size: 21 KiB |
14
docs/public/manifest.webmanifest
Normal file
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,8 +1,21 @@
|
|||
import Logo from "./components/Logo";
|
||||
|
||||
export default {
|
||||
logo: <span>UnstoppableSwap</span>,
|
||||
logo: <Logo />,
|
||||
project: {
|
||||
link: "https://github.com/UnstoppableSwap/core",
|
||||
},
|
||||
head: (
|
||||
<>
|
||||
<title>UnstoppableSwap Docs</title>
|
||||
<meta name="description" content="UnstoppableSwap Docs" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<link rel="icon" href="/icon.svg" type="image/svg+xml" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
</>
|
||||
),
|
||||
primaryHue: 14.3,
|
||||
primarySaturation: 90.68,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Select
|
||||
value={selectedSwap ?? ""}
|
||||
variant="outlined"
|
||||
onChange={(e) => setSelectedSwap(e.target.value as string || null)}
|
||||
style={{ width: "100%" }}
|
||||
displayEmpty
|
||||
>
|
||||
<MenuItem value="">Do not attach a swap</MenuItem>
|
||||
{swaps.map((swap) => (
|
||||
<MenuItem value={swap.swap_id} key={swap.swap_id}>
|
||||
Swap{" "}<TruncatedText>{swap.swap_id}</TruncatedText>{" "}from{" "}
|
||||
{new Date(parseDateString(swap.start_date)).toDateString()} (
|
||||
<PiconeroAmount amount={swap.xmr_amount} />)
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Dialog open={open} onClose={onClose}>
|
||||
<DialogTitle>Submit Feedback</DialogTitle>
|
||||
<DialogContent>
|
||||
<ul>
|
||||
<li>Got something to say? Drop us a message below. </li>
|
||||
<li>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.
|
||||
</li>
|
||||
<li>We appreciate you taking the time to share your thoughts! Every message is read by a core developer!</li>
|
||||
</ul>
|
||||
<Box
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "1rem",
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
value={bodyText}
|
||||
onChange={(e) => setBodyText(e.target.value)}
|
||||
label={
|
||||
bodyTooLong
|
||||
? `Text is too long (${bodyText.length}/${MAX_FEEDBACK_LENGTH})`
|
||||
: "Message"
|
||||
}
|
||||
multiline
|
||||
minRows={4}
|
||||
maxRows={4}
|
||||
fullWidth
|
||||
error={bodyTooLong}
|
||||
/>
|
||||
<Box style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
gap: "1rem",
|
||||
}}>
|
||||
|
||||
<SwapSelectDropDown
|
||||
selectedSwap={selectedSwap}
|
||||
setSelectedSwap={setSelectedSwap}
|
||||
/>
|
||||
<Tooltip title="View the logs">
|
||||
<Box style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<IconButton onClick={() => setSwapLogsEditorOpen(true)} disabled={selectedSwap === null}>
|
||||
<Visibility />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<LogViewer open={swapLogsEditorOpen} setOpen={setSwapLogsEditorOpen} logs={swapLogs} redact={setSwapLogsRedacted} />
|
||||
<Box style={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
gap: "1rem",
|
||||
}}>
|
||||
<Paper variant="outlined" style={{ padding: "0.5rem", width: "100%" }} >
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
return (
|
||||
<Dialog open={open} onClose={handleClose}>
|
||||
<DialogTitle style={{ paddingBottom: '0.5rem' }}>
|
||||
Submit Feedback
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1.5rem',
|
||||
}}
|
||||
>
|
||||
{error && (
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'start',
|
||||
gap: '0.5rem',
|
||||
width: '100%',
|
||||
backgroundColor: 'hsla(0, 45%, 17%, 1)',
|
||||
padding: '0.5rem',
|
||||
borderRadius: '0.5rem',
|
||||
border: '1px solid hsla(0, 61%, 32%, 1)',
|
||||
}}
|
||||
>
|
||||
<ErrorOutline style={{ color: 'hsla(0, 77%, 75%, 1)' }} />
|
||||
<Typography style={{ color: 'hsla(0, 83%, 91%, 1)' }} noWrap>
|
||||
{error}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
<Box>
|
||||
<Typography style={{ marginBottom: '0.5rem' }}>
|
||||
Have a question or need assistance? Message us below
|
||||
or{' '}
|
||||
<ExternalLink href="https://docs.unstoppableswap.net/send_feedback#email-support">
|
||||
email us
|
||||
</ExternalLink>
|
||||
!
|
||||
</Typography>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
value={input.bodyText}
|
||||
onChange={(e) =>
|
||||
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}
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography style={{ marginBottom: '0.5rem' }}>
|
||||
Attach logs with your feedback for better support.
|
||||
</Typography>
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
gap: '1rem',
|
||||
paddingBottom: '0.5rem',
|
||||
}}
|
||||
>
|
||||
<SwapSelectDropDown
|
||||
selectedSwap={input.selectedSwap}
|
||||
setSelectedSwap={(swapId) =>
|
||||
setInputState((prev) => ({
|
||||
...prev,
|
||||
selectedSwap: swapId,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<Tooltip title="View the logs">
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
setSwapLogsEditorOpen(true)
|
||||
}
|
||||
disabled={input.selectedSwap === null}
|
||||
>
|
||||
<Visibility />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<LogViewer
|
||||
open={swapLogsEditorOpen}
|
||||
setOpen={setSwapLogsEditorOpen}
|
||||
logs={logs.swapLogs}
|
||||
setIsRedacted={(redact) =>
|
||||
setInputState((prev) => ({
|
||||
...prev,
|
||||
isSwapLogsRedacted: redact,
|
||||
}))
|
||||
}
|
||||
isRedacted={input.isSwapLogsRedacted}
|
||||
/>
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
gap: '1rem',
|
||||
}}
|
||||
>
|
||||
<Paper
|
||||
variant="outlined"
|
||||
style={{ padding: '0.5rem', width: '100%' }}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
color="primary"
|
||||
checked={input.attachDaemonLogs}
|
||||
onChange={(e) =>
|
||||
setInputState((prev) => ({
|
||||
...prev,
|
||||
attachDaemonLogs:
|
||||
e.target.checked,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Attach logs from the current session"
|
||||
/>
|
||||
</Paper>
|
||||
<Tooltip title="View the logs">
|
||||
<Box
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
setDaemonLogsEditorOpen(true)
|
||||
}
|
||||
disabled={
|
||||
input.attachDaemonLogs === false
|
||||
}
|
||||
>
|
||||
<Visibility />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="textSecondary"
|
||||
style={{ marginBottom: '0.5rem' }}
|
||||
>
|
||||
Your feedback will be answered in the app and can be
|
||||
found in the Feedback tab
|
||||
</Typography>
|
||||
<LogViewer
|
||||
open={daemonLogsEditorOpen}
|
||||
setOpen={setDaemonLogsEditorOpen}
|
||||
logs={logs.daemonLogs}
|
||||
setIsRedacted={(redact) =>
|
||||
setInputState((prev) => ({
|
||||
...prev,
|
||||
isDaemonLogsRedacted: redact,
|
||||
}))
|
||||
}
|
||||
isRedacted={input.isDaemonLogsRedacted}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>Cancel</Button>
|
||||
<PromiseInvokeButton
|
||||
requiresContext={false}
|
||||
color="primary"
|
||||
checked={attachDaemonLogs}
|
||||
onChange={(e) => setAttachDaemonLogs(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label="Attach logs from the current session"
|
||||
/>
|
||||
</Paper>
|
||||
<Tooltip title="View the logs">
|
||||
<Box style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<IconButton onClick={() => setDaemonLogsEditorOpen(true)} disabled={attachDaemonLogs === false}>
|
||||
<Visibility />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<LogViewer open={daemonLogsEditorOpen} setOpen={setDaemonLogsEditorOpen} logs={daemonLogs} redact={setDaemonLogsRedacted} />
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => { clearState(); onClose() }}>Cancel</Button>
|
||||
<LoadingButton
|
||||
color="primary"
|
||||
variant="contained"
|
||||
onClick={sendFeedback}
|
||||
loading={pending}
|
||||
>
|
||||
Submit
|
||||
</LoadingButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function LogViewer(
|
||||
{ open,
|
||||
setOpen,
|
||||
logs,
|
||||
redact
|
||||
}: {
|
||||
open: boolean,
|
||||
setOpen: (_: boolean) => void,
|
||||
logs: (string | CliLog)[] | null,
|
||||
redact: (_: boolean) => void
|
||||
}) {
|
||||
return (
|
||||
<Dialog open={open} onClose={() => setOpen(false)} fullWidth>
|
||||
<DialogContent>
|
||||
<Box>
|
||||
<DialogContentText>
|
||||
<Box style={{ display: "flex", flexDirection: "row", alignItems: "center" }}>
|
||||
<Typography>
|
||||
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.
|
||||
</Typography>
|
||||
|
||||
|
||||
</Box>
|
||||
</DialogContentText>
|
||||
|
||||
<CliLogsBox label="Logs" logs={logs} topRightButton={<Paper style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', paddingLeft: "0.5rem" }} variant="outlined">
|
||||
Redact
|
||||
<Switch color="primary" onChange={(_, checked: boolean) => redact(checked)} />
|
||||
</Paper>} />
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" color="primary" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog >
|
||||
)
|
||||
variant="contained"
|
||||
onInvoke={submitFeedback}
|
||||
onSuccess={handleClose}
|
||||
>
|
||||
Submit
|
||||
</PromiseInvokeButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
66
src-gui/src/renderer/components/modal/feedback/LogViewer.tsx
Normal file
|
|
@ -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 (
|
||||
<Dialog open={open} onClose={() => setOpen(false)} fullWidth>
|
||||
<DialogContent>
|
||||
<Box>
|
||||
<DialogContentText>
|
||||
<Box style={{ display: "flex", flexDirection: "row", alignItems: "center" }}>
|
||||
<Typography>
|
||||
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.
|
||||
</Typography>
|
||||
</Box>
|
||||
</DialogContentText>
|
||||
|
||||
<CliLogsBox
|
||||
label="Logs"
|
||||
logs={logs}
|
||||
topRightButton={
|
||||
<Paper style={{ display: 'flex', justifyContent: 'flex-end', alignItems: 'center', paddingLeft: "0.5rem" }} variant="outlined">
|
||||
Redact
|
||||
<Switch
|
||||
color="primary"
|
||||
checked={isRedacted}
|
||||
onChange={(_, checked: boolean) => setIsRedacted(checked)}
|
||||
/>
|
||||
</Paper>
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button variant="contained" color="primary" onClick={() => setOpen(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<Select
|
||||
value={selectedSwap ?? ""}
|
||||
variant="outlined"
|
||||
onChange={(e) => setSelectedSwap(e.target.value as string || null)}
|
||||
style={{ width: "100%" }}
|
||||
displayEmpty
|
||||
>
|
||||
{swaps.map((swap) => (
|
||||
<MenuItem value={swap.swap_id} key={swap.swap_id}>
|
||||
<Box component="span" style={{ whiteSpace: 'pre' }}>
|
||||
Swap <TruncatedText>{swap.swap_id}</TruncatedText> from{' '}
|
||||
{new Date(parseDateString(swap.start_date)).toDateString()} (
|
||||
<PiconeroAmount amount={swap.xmr_amount} />)
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
<MenuItem value="">Do not attach a swap</MenuItem>
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
163
src-gui/src/renderer/components/modal/feedback/useFeedback.ts
Normal file
|
|
@ -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<FeedbackInputState>({
|
||||
...initialInputState,
|
||||
selectedSwap: currentSwapId?.swap_id || null,
|
||||
})
|
||||
const [logsState, setLogsState] =
|
||||
useState<FeedbackLogsState>(initialLogsState)
|
||||
const [isPending, setIsPending] = useState(false)
|
||||
const [error, setError] = useState<string | null>(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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<PromiseInvokeButton
|
||||
onInvoke={handleExportLogs}
|
||||
{...buttonProps}
|
||||
>
|
||||
Export Logs
|
||||
</PromiseInvokeButton>
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
/>
|
||||
<ExportLogsButton swap_id={swap.swap_id} variant="outlined"
|
||||
size="small"/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -313,3 +313,7 @@ export async function getDataDir(): Promise<string> {
|
|||
export async function resolveApproval(requestId: string, accept: boolean): Promise<void> {
|
||||
await invoke<ResolveApprovalArgs, ResolveApprovalResponse>("resolve_approval_request", { request_id: requestId, accept });
|
||||
}
|
||||
|
||||
export async function saveLogFiles(zipFileName: string, content: Record<string, string>): Promise<void> {
|
||||
await invokeUnsafe<void>("save_txt_files", { zipFileName, content });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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<T, E> to Result<T, String>
|
||||
/// 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<String, String>,
|
||||
) -> 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<PathBuf>
|
||||
.ok_or_else(|| "Dialog cancelled or file path not selected".to_string())?; // Converts to Result<PathBuf, String> 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(
|
||||
|
|
|
|||