Compare commits
4 Commits
da08723fe7
...
b7a48bf13b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7a48bf13b | ||
|
|
e75b045470 | ||
|
|
20375eceb9 | ||
|
|
00deb97a5d |
437
Cargo.lock
generated
437
Cargo.lock
generated
@@ -121,6 +121,126 @@ version = "0.7.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-broadcast"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
|
||||||
|
dependencies = [
|
||||||
|
"event-listener",
|
||||||
|
"event-listener-strategy",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-channel"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
|
||||||
|
dependencies = [
|
||||||
|
"concurrent-queue",
|
||||||
|
"event-listener-strategy",
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-executor"
|
||||||
|
version = "1.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
|
||||||
|
dependencies = [
|
||||||
|
"async-task",
|
||||||
|
"concurrent-queue",
|
||||||
|
"fastrand",
|
||||||
|
"futures-lite",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-io"
|
||||||
|
version = "2.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"cfg-if",
|
||||||
|
"concurrent-queue",
|
||||||
|
"futures-io",
|
||||||
|
"futures-lite",
|
||||||
|
"parking",
|
||||||
|
"polling",
|
||||||
|
"rustix",
|
||||||
|
"slab",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-lock"
|
||||||
|
version = "3.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
|
||||||
|
dependencies = [
|
||||||
|
"event-listener",
|
||||||
|
"event-listener-strategy",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-process"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
|
||||||
|
dependencies = [
|
||||||
|
"async-channel",
|
||||||
|
"async-io",
|
||||||
|
"async-lock",
|
||||||
|
"async-signal",
|
||||||
|
"async-task",
|
||||||
|
"blocking",
|
||||||
|
"cfg-if",
|
||||||
|
"event-listener",
|
||||||
|
"futures-lite",
|
||||||
|
"rustix",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-recursion"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-signal"
|
||||||
|
version = "0.2.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
|
||||||
|
dependencies = [
|
||||||
|
"async-io",
|
||||||
|
"async-lock",
|
||||||
|
"atomic-waker",
|
||||||
|
"cfg-if",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"rustix",
|
||||||
|
"signal-hook-registry",
|
||||||
|
"slab",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-task"
|
||||||
|
version = "4.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.89"
|
version = "0.1.89"
|
||||||
@@ -167,7 +287,7 @@ version = "0.2.14"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"hermit-abi",
|
"hermit-abi 0.1.19",
|
||||||
"libc",
|
"libc",
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
@@ -475,6 +595,19 @@ dependencies = [
|
|||||||
"objc2",
|
"objc2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blocking"
|
||||||
|
version = "1.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
|
||||||
|
dependencies = [
|
||||||
|
"async-channel",
|
||||||
|
"async-task",
|
||||||
|
"futures-io",
|
||||||
|
"futures-lite",
|
||||||
|
"piper",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "brotli"
|
name = "brotli"
|
||||||
version = "8.0.2"
|
version = "8.0.2"
|
||||||
@@ -760,6 +893,15 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "concurrent-queue"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-oid"
|
name = "const-oid"
|
||||||
version = "0.9.6"
|
version = "0.9.6"
|
||||||
@@ -1467,6 +1609,33 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "endi"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enumflags2"
|
||||||
|
version = "0.7.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
|
||||||
|
dependencies = [
|
||||||
|
"enumflags2_derive",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "enumflags2_derive"
|
||||||
|
version = "0.7.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -1494,6 +1663,27 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "event-listener"
|
||||||
|
version = "5.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
|
||||||
|
dependencies = [
|
||||||
|
"concurrent-queue",
|
||||||
|
"parking",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "event-listener-strategy"
|
||||||
|
version = "0.5.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
|
||||||
|
dependencies = [
|
||||||
|
"event-listener",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "failure"
|
name = "failure"
|
||||||
version = "0.1.8"
|
version = "0.1.8"
|
||||||
@@ -1728,6 +1918,19 @@ version = "0.3.32"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-lite"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"parking",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-macro"
|
name = "futures-macro"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -2174,6 +2377,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hermit-abi"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hex"
|
name = "hex"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -2870,6 +3079,18 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mac-notification-sys"
|
||||||
|
version = "0.6.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29a16783dd1a47849b8c8133c9cd3eb2112cfbc6901670af3dba47c8bbfb07d3"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"objc2",
|
||||||
|
"objc2-foundation",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mach2"
|
name = "mach2"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -3129,6 +3350,20 @@ dependencies = [
|
|||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "notify-rust"
|
||||||
|
version = "4.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b2c9bc1689653cfbc04400b8719f2562638ff9c545bbd48cc58c657a14526df"
|
||||||
|
dependencies = [
|
||||||
|
"futures-lite",
|
||||||
|
"log",
|
||||||
|
"mac-notification-sys",
|
||||||
|
"serde",
|
||||||
|
"tauri-winrt-notification",
|
||||||
|
"zbus",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.50.3"
|
version = "0.50.3"
|
||||||
@@ -3275,6 +3510,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"block2",
|
"block2",
|
||||||
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
@@ -3452,6 +3688,16 @@ dependencies = [
|
|||||||
"cmake",
|
"cmake",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ordered-stream"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "os_pipe"
|
name = "os_pipe"
|
||||||
version = "1.2.3"
|
version = "1.2.3"
|
||||||
@@ -3493,6 +3739,12 @@ dependencies = [
|
|||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "parking"
|
||||||
|
version = "2.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.5"
|
version = "0.12.5"
|
||||||
@@ -3731,6 +3983,17 @@ version = "0.2.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "piper"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"fastrand",
|
||||||
|
"futures-io",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkcs8"
|
name = "pkcs8"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
@@ -3755,7 +4018,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"indexmap 2.14.0",
|
"indexmap 2.14.0",
|
||||||
"quick-xml",
|
"quick-xml 0.38.4",
|
||||||
"serde",
|
"serde",
|
||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
@@ -3773,6 +4036,20 @@ dependencies = [
|
|||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "polling"
|
||||||
|
version = "3.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"concurrent-queue",
|
||||||
|
"hermit-abi 0.5.2",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "poly1305"
|
name = "poly1305"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -3922,6 +4199,15 @@ version = "2.28.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94"
|
checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-xml"
|
||||||
|
version = "0.37.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.38.4"
|
version = "0.38.4"
|
||||||
@@ -5377,6 +5663,25 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-notification"
|
||||||
|
version = "2.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"notify-rust",
|
||||||
|
"rand 0.9.2",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"time",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-shell"
|
name = "tauri-plugin-shell"
|
||||||
version = "2.3.5"
|
version = "2.3.5"
|
||||||
@@ -5498,6 +5803,18 @@ dependencies = [
|
|||||||
"toml 0.9.12+spec-1.1.0",
|
"toml 0.9.12+spec-1.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-winrt-notification"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9"
|
||||||
|
dependencies = [
|
||||||
|
"quick-xml 0.37.5",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"windows 0.61.3",
|
||||||
|
"windows-version",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.27.0"
|
version = "3.27.0"
|
||||||
@@ -6065,6 +6382,17 @@ version = "1.19.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uds_windows"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||||
|
dependencies = [
|
||||||
|
"memoffset",
|
||||||
|
"tempfile",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unic-char-property"
|
name = "unic-char-property"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -7121,6 +7449,9 @@ name = "winnow"
|
|||||||
version = "0.7.15"
|
version = "0.7.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
@@ -7381,6 +7712,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-plugin-notification",
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -7566,6 +7898,67 @@ dependencies = [
|
|||||||
"synstructure 0.13.2",
|
"synstructure 0.13.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zbus"
|
||||||
|
version = "5.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc"
|
||||||
|
dependencies = [
|
||||||
|
"async-broadcast",
|
||||||
|
"async-executor",
|
||||||
|
"async-io",
|
||||||
|
"async-lock",
|
||||||
|
"async-process",
|
||||||
|
"async-recursion",
|
||||||
|
"async-task",
|
||||||
|
"async-trait",
|
||||||
|
"blocking",
|
||||||
|
"enumflags2",
|
||||||
|
"event-listener",
|
||||||
|
"futures-core",
|
||||||
|
"futures-lite",
|
||||||
|
"hex",
|
||||||
|
"libc",
|
||||||
|
"ordered-stream",
|
||||||
|
"rustix",
|
||||||
|
"serde",
|
||||||
|
"serde_repr",
|
||||||
|
"tracing",
|
||||||
|
"uds_windows",
|
||||||
|
"uuid",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
"winnow 0.7.15",
|
||||||
|
"zbus_macros",
|
||||||
|
"zbus_names",
|
||||||
|
"zvariant",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zbus_macros"
|
||||||
|
version = "5.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-crate 3.5.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
"zbus_names",
|
||||||
|
"zvariant",
|
||||||
|
"zvariant_utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zbus_names"
|
||||||
|
version = "4.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"winnow 0.7.15",
|
||||||
|
"zvariant",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.48"
|
version = "0.8.48"
|
||||||
@@ -7665,3 +8058,43 @@ name = "zmij"
|
|||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zvariant"
|
||||||
|
version = "5.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b"
|
||||||
|
dependencies = [
|
||||||
|
"endi",
|
||||||
|
"enumflags2",
|
||||||
|
"serde",
|
||||||
|
"winnow 0.7.15",
|
||||||
|
"zvariant_derive",
|
||||||
|
"zvariant_utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zvariant_derive"
|
||||||
|
version = "5.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro-crate 3.5.0",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
"zvariant_utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zvariant_utils"
|
||||||
|
version = "3.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"serde",
|
||||||
|
"syn 2.0.117",
|
||||||
|
"winnow 0.7.15",
|
||||||
|
]
|
||||||
|
|||||||
@@ -275,14 +275,63 @@ pub fn determine_role(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the address is in an RFC1918 / link-local /
|
||||||
|
/// loopback range and therefore cannot possibly be a post-NAT
|
||||||
|
/// reflex address from the public internet's point of view.
|
||||||
|
///
|
||||||
|
/// A probe against a relay ON THE SAME LAN as the client will
|
||||||
|
/// naturally report the client's LAN IP back (because there's no
|
||||||
|
/// NAT between them) — that observation is real but says nothing
|
||||||
|
/// about the client's public-internet-facing NAT state. Mixing
|
||||||
|
/// LAN reflex addrs with public-internet reflex addrs in
|
||||||
|
/// `classify_nat` would always report `Multiple` (different IPs)
|
||||||
|
/// and falsely warn about symmetric NAT. Filter them out before
|
||||||
|
/// classifying.
|
||||||
|
fn is_private_or_loopback(addr: &SocketAddr) -> bool {
|
||||||
|
match addr.ip() {
|
||||||
|
std::net::IpAddr::V4(v4) => {
|
||||||
|
let o = v4.octets();
|
||||||
|
v4.is_loopback()
|
||||||
|
|| v4.is_private() // 10/8, 172.16/12, 192.168/16
|
||||||
|
|| v4.is_link_local() // 169.254/16
|
||||||
|
|| (o[0] == 100 && (o[1] & 0xc0) == 0x40) // 100.64/10 CGNAT shared
|
||||||
|
}
|
||||||
|
std::net::IpAddr::V6(v6) => {
|
||||||
|
v6.is_loopback() || v6.is_unspecified() || (v6.segments()[0] & 0xffc0) == 0xfe80 // fe80::/10 link-local
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Pure-function NAT classifier — split out for unit testing
|
/// Pure-function NAT classifier — split out for unit testing
|
||||||
/// without touching the network.
|
/// without touching the network.
|
||||||
|
///
|
||||||
|
/// Only considers probes whose reflex addr is a **public-internet**
|
||||||
|
/// address. LAN / private / loopback reflex addrs are dropped
|
||||||
|
/// because they reflect the same-network path rather than the
|
||||||
|
/// real NAT state. CGNAT (100.64/10) is also treated as private
|
||||||
|
/// because the post-CGNAT address would be what we actually want
|
||||||
|
/// to classify on — but CGNAT is unreachable from outside the
|
||||||
|
/// carrier, so a relay seeing the CGNAT addr is on the same
|
||||||
|
/// carrier network and again not useful for classification.
|
||||||
pub fn classify_nat(probes: &[NatProbeResult]) -> (NatType, Option<String>) {
|
pub fn classify_nat(probes: &[NatProbeResult]) -> (NatType, Option<String>) {
|
||||||
let successes: Vec<SocketAddr> = probes
|
// First: parse every successful probe's observed addr.
|
||||||
|
let parsed: Vec<SocketAddr> = probes
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|p| p.observed_addr.as_deref().and_then(|s| s.parse().ok()))
|
.filter_map(|p| p.observed_addr.as_deref().and_then(|s| s.parse().ok()))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
// Then: drop LAN / private / loopback reflex addrs. Those are
|
||||||
|
// legitimate observations by same-network relays, but they
|
||||||
|
// don't contribute to NAT-type classification because the
|
||||||
|
// client's real public-facing NAT mapping is not involved on
|
||||||
|
// that path. A relay on the same LAN always sees the client's
|
||||||
|
// LAN IP, regardless of whether the NAT beyond it is cone or
|
||||||
|
// symmetric.
|
||||||
|
let successes: Vec<SocketAddr> = parsed
|
||||||
|
.into_iter()
|
||||||
|
.filter(|a| !is_private_or_loopback(a))
|
||||||
|
.collect();
|
||||||
|
|
||||||
if successes.len() < 2 {
|
if successes.len() < 2 {
|
||||||
return (NatType::Unknown, None);
|
return (NatType::Unknown, None);
|
||||||
}
|
}
|
||||||
@@ -365,6 +414,66 @@ mod tests {
|
|||||||
assert!(addr.is_none());
|
assert!(addr.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classify_drops_private_ip_probes() {
|
||||||
|
// One LAN probe + one public probe should behave like a
|
||||||
|
// single public probe — i.e. Unknown (not enough data to
|
||||||
|
// classify). This is the common real-world case: the user
|
||||||
|
// has a LAN relay + an internet relay configured, the LAN
|
||||||
|
// relay sees the LAN IP, the internet relay sees the WAN
|
||||||
|
// IP, and the old classifier would flag "Multiple" and
|
||||||
|
// falsely warn about symmetric NAT.
|
||||||
|
let probes = vec![
|
||||||
|
mk(Some("192.168.1.100:4433")), // LAN — must be dropped
|
||||||
|
mk(Some("203.0.113.5:4433")), // public (TEST-NET-3)
|
||||||
|
];
|
||||||
|
let (nt, _) = classify_nat(&probes);
|
||||||
|
assert_eq!(nt, NatType::Unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classify_drops_loopback_probes() {
|
||||||
|
let probes = vec![
|
||||||
|
mk(Some("127.0.0.1:4433")), // loopback — must be dropped
|
||||||
|
mk(Some("203.0.113.5:4433")), // public
|
||||||
|
mk(Some("203.0.113.5:4433")), // public, same addr
|
||||||
|
];
|
||||||
|
let (nt, addr) = classify_nat(&probes);
|
||||||
|
// Two public probes with identical addrs → Cone.
|
||||||
|
assert_eq!(nt, NatType::Cone);
|
||||||
|
assert_eq!(addr.as_deref(), Some("203.0.113.5:4433"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classify_drops_cgnat_probes() {
|
||||||
|
// 100.64.0.0/10 is the CGNAT shared-transition range.
|
||||||
|
// Filter treats it like RFC1918 — a relay that sees the
|
||||||
|
// client with a 100.64/10 addr is on the same CGNAT
|
||||||
|
// network and can't contribute to public NAT classification.
|
||||||
|
let probes = vec![
|
||||||
|
mk(Some("100.64.0.42:4433")), // CGNAT — dropped
|
||||||
|
mk(Some("203.0.113.5:4433")), // public
|
||||||
|
mk(Some("203.0.113.5:12345")), // public, different port
|
||||||
|
];
|
||||||
|
let (nt, _) = classify_nat(&probes);
|
||||||
|
// Two public probes same IP different port → SymmetricPort.
|
||||||
|
assert_eq!(nt, NatType::SymmetricPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn classify_two_lan_probes_is_unknown_not_cone() {
|
||||||
|
// Even if both probes come back from LAN relays, we can't
|
||||||
|
// say anything useful about the public NAT state. Unknown,
|
||||||
|
// not Cone.
|
||||||
|
let probes = vec![
|
||||||
|
mk(Some("192.168.1.100:4433")),
|
||||||
|
mk(Some("192.168.1.100:4433")),
|
||||||
|
];
|
||||||
|
let (nt, addr) = classify_nat(&probes);
|
||||||
|
assert_eq!(nt, NatType::Unknown);
|
||||||
|
assert!(addr.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn classify_mix_of_success_and_failure() {
|
fn classify_mix_of_success_and_failure() {
|
||||||
let probes = vec![
|
let probes = vec![
|
||||||
|
|||||||
@@ -116,11 +116,19 @@ async fn probe_reflect_addr_happy_path() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
// Test 2: two loopback relays → Cone classification
|
// Test 2: two loopback relays → probes succeed, classification is Unknown
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// With the private-IP filter added in the NAT classifier, loopback
|
||||||
|
// reflex addrs (127.0.0.1) are dropped before classification —
|
||||||
|
// they can't possibly indicate public-internet NAT state. So the
|
||||||
|
// test now asserts:
|
||||||
|
// - both probes succeed end-to-end (wire plumbing works)
|
||||||
|
// - both return 127.0.0.1 (same-host is visible)
|
||||||
|
// - the aggregated verdict is Unknown (no public probes)
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||||
async fn detect_nat_type_two_loopback_relays_is_cone() {
|
async fn detect_nat_type_two_loopback_relays_probes_work_but_classify_unknown() {
|
||||||
let (addr_a, _h_a) = spawn_mock_relay().await;
|
let (addr_a, _h_a) = spawn_mock_relay().await;
|
||||||
let (addr_b, _h_b) = spawn_mock_relay().await;
|
let (addr_b, _h_b) = spawn_mock_relay().await;
|
||||||
|
|
||||||
@@ -135,24 +143,13 @@ async fn detect_nat_type_two_loopback_relays_is_cone() {
|
|||||||
|
|
||||||
assert_eq!(detection.probes.len(), 2);
|
assert_eq!(detection.probes.len(), 2);
|
||||||
for p in &detection.probes {
|
for p in &detection.probes {
|
||||||
assert!(p.observed_addr.is_some(), "probe {:?} failed: {:?}", p.relay_name, p.error);
|
assert!(
|
||||||
|
p.observed_addr.is_some(),
|
||||||
|
"probe {:?} failed: {:?}",
|
||||||
|
p.relay_name,
|
||||||
|
p.error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loopback single-host: every probe sees 127.0.0.1 and, crucially,
|
|
||||||
// uses a different ephemeral source port (since probe_reflect_addr
|
|
||||||
// spins up a fresh quinn::Endpoint per probe). Wait — that makes
|
|
||||||
// this look like Symmetric to the classifier, not Cone!
|
|
||||||
//
|
|
||||||
// The classifier cares about the *observed* addr, which is what
|
|
||||||
// the relay sees as the client's source. Two different client
|
|
||||||
// endpoints on loopback → two different observed ports → the
|
|
||||||
// classifier correctly labels this as SymmetricPort in the test
|
|
||||||
// environment. That's still a valid verification of the
|
|
||||||
// plumbing, just not of the Cone classification.
|
|
||||||
//
|
|
||||||
// Accept either Cone OR SymmetricPort for this test, then
|
|
||||||
// assert the more specific invariant that matters: both probes
|
|
||||||
// returned the same observed IP.
|
|
||||||
let observed_ips: Vec<String> = detection
|
let observed_ips: Vec<String> = detection
|
||||||
.probes
|
.probes
|
||||||
.iter()
|
.iter()
|
||||||
@@ -167,14 +164,15 @@ async fn detect_nat_type_two_loopback_relays_is_cone() {
|
|||||||
assert_eq!(observed_ips[0], "127.0.0.1");
|
assert_eq!(observed_ips[0], "127.0.0.1");
|
||||||
assert_eq!(observed_ips[1], "127.0.0.1");
|
assert_eq!(observed_ips[1], "127.0.0.1");
|
||||||
|
|
||||||
// Either classification is valid on loopback (see long comment
|
// Classification: loopback probes are filtered out of the
|
||||||
// above). Explicitly assert the set so a future refactor that
|
// public-NAT classifier, so with 0 public probes the result
|
||||||
// accidentally returns `Multiple` or `Unknown` fails the test.
|
// is Unknown.
|
||||||
assert!(
|
assert_eq!(
|
||||||
matches!(detection.nat_type, NatType::Cone | NatType::SymmetricPort),
|
detection.nat_type,
|
||||||
"expected Cone or SymmetricPort on loopback, got {:?}",
|
NatType::Unknown,
|
||||||
detection.nat_type
|
"loopback-only probes must not contribute to public NAT classification"
|
||||||
);
|
);
|
||||||
|
assert!(detection.consensus_addr.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ tauri-build = { version = "2", features = [] }
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = [] }
|
||||||
tauri-plugin-shell = "2"
|
tauri-plugin-shell = "2"
|
||||||
|
tauri-plugin-notification = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
"core:window:default",
|
"core:window:default",
|
||||||
"core:app:default",
|
"core:app:default",
|
||||||
"core:webview:default",
|
"core:webview:default",
|
||||||
"shell:default"
|
"shell:default",
|
||||||
|
"notification:default",
|
||||||
|
"notification:allow-notify",
|
||||||
|
"notification:allow-request-permission",
|
||||||
|
"notification:allow-is-permission-granted"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
{"default":{"identifier":"default","description":"Default capability — grants core APIs (events, path, window, app, clipboard) to the main window on every platform we ship to.","local":true,"windows":["main"],"permissions":["core:default","core:event:default","core:event:allow-listen","core:event:allow-unlisten","core:event:allow-emit","core:event:allow-emit-to","core:path:default","core:window:default","core:app:default","core:webview:default","shell:default"],"platforms":["linux","macOS","windows","android","iOS"]}}
|
{"default":{"identifier":"default","description":"Default capability — grants core APIs (events, path, window, app, clipboard) to the main window on every platform we ship to.","local":true,"windows":["main"],"permissions":["core:default","core:event:default","core:event:allow-listen","core:event:allow-unlisten","core:event:allow-emit","core:event:allow-emit-to","core:path:default","core:window:default","core:app:default","core:webview:default","shell:default","notification:default","notification:allow-notify","notification:allow-request-permission","notification:allow-is-permission-granted"],"platforms":["linux","macOS","windows","android","iOS"]}}
|
||||||
@@ -2354,6 +2354,204 @@
|
|||||||
"const": "core:window:deny-unminimize",
|
"const": "core:window:deny-unminimize",
|
||||||
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
|
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:default",
|
||||||
|
"markdownDescription": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the batch command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-batch",
|
||||||
|
"markdownDescription": "Enables the batch command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the cancel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-cancel",
|
||||||
|
"markdownDescription": "Enables the cancel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the check_permissions command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-check-permissions",
|
||||||
|
"markdownDescription": "Enables the check_permissions command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the create_channel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-create-channel",
|
||||||
|
"markdownDescription": "Enables the create_channel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the delete_channel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-delete-channel",
|
||||||
|
"markdownDescription": "Enables the delete_channel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the get_active command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-get-active",
|
||||||
|
"markdownDescription": "Enables the get_active command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the get_pending command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-get-pending",
|
||||||
|
"markdownDescription": "Enables the get_pending command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the is_permission_granted command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-is-permission-granted",
|
||||||
|
"markdownDescription": "Enables the is_permission_granted command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the list_channels command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-list-channels",
|
||||||
|
"markdownDescription": "Enables the list_channels command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the notify command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-notify",
|
||||||
|
"markdownDescription": "Enables the notify command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the permission_state command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-permission-state",
|
||||||
|
"markdownDescription": "Enables the permission_state command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the register_action_types command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-register-action-types",
|
||||||
|
"markdownDescription": "Enables the register_action_types command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the register_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-register-listener",
|
||||||
|
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the remove_active command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-remove-active",
|
||||||
|
"markdownDescription": "Enables the remove_active command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the request_permission command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-request-permission",
|
||||||
|
"markdownDescription": "Enables the request_permission command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the show command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-show",
|
||||||
|
"markdownDescription": "Enables the show command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the batch command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-batch",
|
||||||
|
"markdownDescription": "Denies the batch command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the cancel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-cancel",
|
||||||
|
"markdownDescription": "Denies the cancel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the check_permissions command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-check-permissions",
|
||||||
|
"markdownDescription": "Denies the check_permissions command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the create_channel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-create-channel",
|
||||||
|
"markdownDescription": "Denies the create_channel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the delete_channel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-delete-channel",
|
||||||
|
"markdownDescription": "Denies the delete_channel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the get_active command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-get-active",
|
||||||
|
"markdownDescription": "Denies the get_active command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the get_pending command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-get-pending",
|
||||||
|
"markdownDescription": "Denies the get_pending command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the is_permission_granted command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-is-permission-granted",
|
||||||
|
"markdownDescription": "Denies the is_permission_granted command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the list_channels command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-list-channels",
|
||||||
|
"markdownDescription": "Denies the list_channels command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the notify command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-notify",
|
||||||
|
"markdownDescription": "Denies the notify command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the permission_state command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-permission-state",
|
||||||
|
"markdownDescription": "Denies the permission_state command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the register_action_types command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-register-action-types",
|
||||||
|
"markdownDescription": "Denies the register_action_types command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the register_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-register-listener",
|
||||||
|
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the remove_active command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-remove-active",
|
||||||
|
"markdownDescription": "Denies the remove_active command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the request_permission command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-request-permission",
|
||||||
|
"markdownDescription": "Denies the request_permission command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the show command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-show",
|
||||||
|
"markdownDescription": "Denies the show command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
|
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -2354,6 +2354,204 @@
|
|||||||
"const": "core:window:deny-unminimize",
|
"const": "core:window:deny-unminimize",
|
||||||
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
|
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:default",
|
||||||
|
"markdownDescription": "This permission set configures which\nnotification features are by default exposed.\n\n#### Granted Permissions\n\nIt allows all notification related features.\n\n\n#### This default permission set includes:\n\n- `allow-is-permission-granted`\n- `allow-request-permission`\n- `allow-notify`\n- `allow-register-action-types`\n- `allow-register-listener`\n- `allow-cancel`\n- `allow-get-pending`\n- `allow-remove-active`\n- `allow-get-active`\n- `allow-check-permissions`\n- `allow-show`\n- `allow-batch`\n- `allow-list-channels`\n- `allow-delete-channel`\n- `allow-create-channel`\n- `allow-permission-state`"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the batch command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-batch",
|
||||||
|
"markdownDescription": "Enables the batch command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the cancel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-cancel",
|
||||||
|
"markdownDescription": "Enables the cancel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the check_permissions command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-check-permissions",
|
||||||
|
"markdownDescription": "Enables the check_permissions command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the create_channel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-create-channel",
|
||||||
|
"markdownDescription": "Enables the create_channel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the delete_channel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-delete-channel",
|
||||||
|
"markdownDescription": "Enables the delete_channel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the get_active command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-get-active",
|
||||||
|
"markdownDescription": "Enables the get_active command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the get_pending command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-get-pending",
|
||||||
|
"markdownDescription": "Enables the get_pending command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the is_permission_granted command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-is-permission-granted",
|
||||||
|
"markdownDescription": "Enables the is_permission_granted command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the list_channels command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-list-channels",
|
||||||
|
"markdownDescription": "Enables the list_channels command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the notify command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-notify",
|
||||||
|
"markdownDescription": "Enables the notify command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the permission_state command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-permission-state",
|
||||||
|
"markdownDescription": "Enables the permission_state command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the register_action_types command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-register-action-types",
|
||||||
|
"markdownDescription": "Enables the register_action_types command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the register_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-register-listener",
|
||||||
|
"markdownDescription": "Enables the register_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the remove_active command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-remove-active",
|
||||||
|
"markdownDescription": "Enables the remove_active command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the request_permission command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-request-permission",
|
||||||
|
"markdownDescription": "Enables the request_permission command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Enables the show command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:allow-show",
|
||||||
|
"markdownDescription": "Enables the show command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the batch command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-batch",
|
||||||
|
"markdownDescription": "Denies the batch command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the cancel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-cancel",
|
||||||
|
"markdownDescription": "Denies the cancel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the check_permissions command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-check-permissions",
|
||||||
|
"markdownDescription": "Denies the check_permissions command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the create_channel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-create-channel",
|
||||||
|
"markdownDescription": "Denies the create_channel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the delete_channel command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-delete-channel",
|
||||||
|
"markdownDescription": "Denies the delete_channel command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the get_active command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-get-active",
|
||||||
|
"markdownDescription": "Denies the get_active command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the get_pending command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-get-pending",
|
||||||
|
"markdownDescription": "Denies the get_pending command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the is_permission_granted command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-is-permission-granted",
|
||||||
|
"markdownDescription": "Denies the is_permission_granted command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the list_channels command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-list-channels",
|
||||||
|
"markdownDescription": "Denies the list_channels command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the notify command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-notify",
|
||||||
|
"markdownDescription": "Denies the notify command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the permission_state command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-permission-state",
|
||||||
|
"markdownDescription": "Denies the permission_state command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the register_action_types command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-register-action-types",
|
||||||
|
"markdownDescription": "Denies the register_action_types command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the register_listener command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-register-listener",
|
||||||
|
"markdownDescription": "Denies the register_listener command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the remove_active command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-remove-active",
|
||||||
|
"markdownDescription": "Denies the remove_active command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the request_permission command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-request-permission",
|
||||||
|
"markdownDescription": "Denies the request_permission command without any pre-configured scope."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Denies the show command without any pre-configured scope.",
|
||||||
|
"type": "string",
|
||||||
|
"const": "notification:deny-show",
|
||||||
|
"markdownDescription": "Denies the show command without any pre-configured scope."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
|
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@@ -648,6 +648,23 @@ struct SignalState {
|
|||||||
/// Tauri command to compute the deterministic role for the
|
/// Tauri command to compute the deterministic role for the
|
||||||
/// dual-path QUIC race against `peer_direct_addr`.
|
/// dual-path QUIC race against `peer_direct_addr`.
|
||||||
own_reflex_addr: Option<String>,
|
own_reflex_addr: Option<String>,
|
||||||
|
/// The relay address the user currently wants to be registered
|
||||||
|
/// against. `Some` means "keep me connected" — the supervisor
|
||||||
|
/// will auto-reconnect after unexpected drops. `None` means
|
||||||
|
/// "user explicitly deregistered" — do not retry.
|
||||||
|
///
|
||||||
|
/// Distinguishing these two cases is what lets relay
|
||||||
|
/// restarts + transient network blips be transparent to the
|
||||||
|
/// user: the recv loop dies, but because `desired_relay_addr`
|
||||||
|
/// is still set, a supervisor task retries the full
|
||||||
|
/// connect+register flow with exponential backoff until the
|
||||||
|
/// relay is reachable again.
|
||||||
|
desired_relay_addr: Option<String>,
|
||||||
|
/// Single-flight guard: `true` while the reconnect supervisor
|
||||||
|
/// task is actively trying to re-establish the signal
|
||||||
|
/// connection. Prevents duplicate supervisors from spawning
|
||||||
|
/// (recv loop exit races with a manual register_signal call).
|
||||||
|
reconnect_in_progress: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -656,6 +673,80 @@ async fn register_signal(
|
|||||||
app: tauri::AppHandle,
|
app: tauri::AppHandle,
|
||||||
relay: String,
|
relay: String,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
|
// Set the desired relay and handle the "already registered to
|
||||||
|
// a different relay" transition. This is the public entry
|
||||||
|
// point — settings-screen changes come through here.
|
||||||
|
let already_same = {
|
||||||
|
let sig = state.signal.lock().await;
|
||||||
|
sig.transport.is_some()
|
||||||
|
&& sig.desired_relay_addr.as_deref() == Some(relay.as_str())
|
||||||
|
};
|
||||||
|
if already_same {
|
||||||
|
// Idempotent: user hit "Register" twice on the same relay,
|
||||||
|
// or the JS side re-called after a settings save that
|
||||||
|
// didn't actually change the relay.
|
||||||
|
let sig = state.signal.lock().await;
|
||||||
|
return Ok(sig.fingerprint.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tear down any existing registration (different relay → swap).
|
||||||
|
internal_deregister(&state.signal, /*keep_desired=*/ false).await;
|
||||||
|
|
||||||
|
// Announce the new desired state so the recv-loop exit path and
|
||||||
|
// any running supervisor can see it.
|
||||||
|
{
|
||||||
|
let mut sig = state.signal.lock().await;
|
||||||
|
sig.desired_relay_addr = Some(relay.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
do_register_signal(state.signal.clone(), app, relay).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close the current signal transport + clear derived state.
|
||||||
|
/// Used by `deregister` (with `keep_desired = false`, clearing
|
||||||
|
/// `desired_relay_addr`) and by the relay-swap path in
|
||||||
|
/// `register_signal` (also `keep_desired = false` — the caller
|
||||||
|
/// is about to set a new desired addr).
|
||||||
|
async fn internal_deregister(
|
||||||
|
signal_state: &Arc<tokio::sync::Mutex<SignalState>>,
|
||||||
|
keep_desired: bool,
|
||||||
|
) {
|
||||||
|
let mut sig = signal_state.lock().await;
|
||||||
|
if let Some(t) = sig.transport.take() {
|
||||||
|
// Dropping the transport Arc closes the quinn connection;
|
||||||
|
// calling close() explicitly is a no-op but neat.
|
||||||
|
let _ = t.close().await;
|
||||||
|
}
|
||||||
|
sig.endpoint = None;
|
||||||
|
sig.signal_status = "idle".into();
|
||||||
|
sig.incoming_call_id = None;
|
||||||
|
sig.incoming_caller_fp = None;
|
||||||
|
sig.incoming_caller_alias = None;
|
||||||
|
sig.pending_reflect = None;
|
||||||
|
sig.own_reflex_addr = None;
|
||||||
|
if !keep_desired {
|
||||||
|
sig.desired_relay_addr = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Core register flow, extracted so the Tauri command AND the
|
||||||
|
/// reconnect supervisor can both call it. Does the connect +
|
||||||
|
/// RegisterPresence + spawn-recv-loop dance.
|
||||||
|
///
|
||||||
|
/// Contract: `signal_state.desired_relay_addr` must already be
|
||||||
|
/// set to `Some(relay)` by the caller. On recv-loop exit, the
|
||||||
|
/// spawned task will check `desired_relay_addr` and (if still
|
||||||
|
/// Some) trigger the reconnect supervisor.
|
||||||
|
///
|
||||||
|
/// Explicit `+ Send` on the return type so the reconnect
|
||||||
|
/// supervisor (which lives inside a `tokio::spawn`) can await
|
||||||
|
/// this future without hitting auto-trait inference issues.
|
||||||
|
fn do_register_signal(
|
||||||
|
signal_state: Arc<tokio::sync::Mutex<SignalState>>,
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
relay: String,
|
||||||
|
) -> impl std::future::Future<Output = Result<String, String>> + Send {
|
||||||
|
async move {
|
||||||
use wzp_proto::SignalMessage;
|
use wzp_proto::SignalMessage;
|
||||||
|
|
||||||
emit_call_debug(&app, "register_signal:start", serde_json::json!({ "relay": relay }));
|
emit_call_debug(&app, "register_signal:start", serde_json::json!({ "relay": relay }));
|
||||||
@@ -696,13 +787,27 @@ async fn register_signal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{ let mut sig = state.signal.lock().await; sig.transport = Some(transport.clone()); sig.endpoint = Some(endpoint.clone()); sig.fingerprint = fp.clone(); sig.signal_status = "registered".into(); }
|
{
|
||||||
|
let mut sig = signal_state.lock().await;
|
||||||
|
sig.transport = Some(transport.clone());
|
||||||
|
sig.endpoint = Some(endpoint.clone());
|
||||||
|
sig.fingerprint = fp.clone();
|
||||||
|
sig.signal_status = "registered".into();
|
||||||
|
}
|
||||||
|
// Let the JS side know we've (re-)entered "registered" so any
|
||||||
|
// "reconnecting..." banner can clear.
|
||||||
|
let _ = app.emit(
|
||||||
|
"signal-event",
|
||||||
|
serde_json::json!({ "type": "registered", "fingerprint": fp }),
|
||||||
|
);
|
||||||
|
|
||||||
tracing::info!(%fp, "signal registered, spawning recv loop");
|
tracing::info!(%fp, "signal registered, spawning recv loop");
|
||||||
emit_call_debug(&app, "register_signal:recv_loop_spawning", serde_json::json!({ "fingerprint": fp }));
|
emit_call_debug(&app, "register_signal:recv_loop_spawning", serde_json::json!({ "fingerprint": fp }));
|
||||||
let signal_state = Arc::clone(&state.signal);
|
let signal_state_loop = signal_state.clone();
|
||||||
let app_clone = app.clone();
|
let app_clone = app.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
// Capture for the exit-path reconnect trigger below.
|
||||||
|
let signal_state = signal_state_loop.clone();
|
||||||
loop {
|
loop {
|
||||||
match transport.recv_signal().await {
|
match transport.recv_signal().await {
|
||||||
Ok(Some(SignalMessage::CallRinging { call_id })) => {
|
Ok(Some(SignalMessage::CallRinging { call_id })) => {
|
||||||
@@ -837,9 +942,165 @@ async fn register_signal(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
tracing::warn!("signal recv loop exited — signal_status=idle, transport dropped");
|
tracing::warn!("signal recv loop exited — signal_status=idle, transport dropped");
|
||||||
let mut sig = signal_state.lock().await; sig.signal_status = "idle".into(); sig.transport = None;
|
// Determine whether this was a user-requested close or an
|
||||||
|
// unexpected drop. `desired_relay_addr.is_some()` means the
|
||||||
|
// user still wants to be registered — spawn the reconnect
|
||||||
|
// supervisor with exponential backoff.
|
||||||
|
let (should_reconnect, desired_relay, already_reconnecting) = {
|
||||||
|
let mut sig = signal_state.lock().await;
|
||||||
|
sig.signal_status = "idle".into();
|
||||||
|
sig.transport = None;
|
||||||
|
(
|
||||||
|
sig.desired_relay_addr.is_some(),
|
||||||
|
sig.desired_relay_addr.clone(),
|
||||||
|
sig.reconnect_in_progress,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
if should_reconnect && !already_reconnecting {
|
||||||
|
if let Some(relay) = desired_relay {
|
||||||
|
tracing::info!(%relay, "signal recv loop exited unexpectedly — spawning reconnect supervisor");
|
||||||
|
emit_call_debug(
|
||||||
|
&app_clone,
|
||||||
|
"signal:reconnect_supervisor_spawning",
|
||||||
|
serde_json::json!({ "relay": relay }),
|
||||||
|
);
|
||||||
|
let _ = app_clone.emit(
|
||||||
|
"signal-event",
|
||||||
|
serde_json::json!({ "type": "reconnecting", "relay": relay }),
|
||||||
|
);
|
||||||
|
let state_for_sup = signal_state.clone();
|
||||||
|
let app_for_sup = app_clone.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
signal_reconnect_supervisor(state_for_sup, app_for_sup, relay).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if should_reconnect && already_reconnecting {
|
||||||
|
tracing::debug!("signal recv loop exited; reconnect supervisor already running");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
Ok(fp)
|
Ok(fp)
|
||||||
|
} // end async move
|
||||||
|
} // end fn do_register_signal
|
||||||
|
|
||||||
|
/// Supervisor task: loops with exponential backoff, calling
|
||||||
|
/// `do_register_signal` until the relay comes back online. Exits
|
||||||
|
/// as soon as one attempt succeeds (the newly-spawned recv loop
|
||||||
|
/// owns the connection from that point on) OR the user clears
|
||||||
|
/// `desired_relay_addr` via `deregister`.
|
||||||
|
///
|
||||||
|
/// Backoff schedule: 1s, 2s, 4s, 8s, 15s, 30s (capped). Reset on
|
||||||
|
/// success or exit.
|
||||||
|
async fn signal_reconnect_supervisor(
|
||||||
|
signal_state: Arc<tokio::sync::Mutex<SignalState>>,
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
initial_relay: String,
|
||||||
|
) {
|
||||||
|
// Claim the single-flight slot so a second exit-path trigger
|
||||||
|
// or a manual register_signal doesn't spawn a duplicate.
|
||||||
|
{
|
||||||
|
let mut sig = signal_state.lock().await;
|
||||||
|
if sig.reconnect_in_progress {
|
||||||
|
tracing::debug!("reconnect supervisor: another already running, exiting");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sig.reconnect_in_progress = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let backoff_schedule_ms: [u64; 6] = [1_000, 2_000, 4_000, 8_000, 15_000, 30_000];
|
||||||
|
let mut attempt: usize = 0;
|
||||||
|
let mut current_relay = initial_relay;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Has the user cleared the desired relay? If so, exit.
|
||||||
|
let (desired, transport_is_some) = {
|
||||||
|
let sig = signal_state.lock().await;
|
||||||
|
(sig.desired_relay_addr.clone(), sig.transport.is_some())
|
||||||
|
};
|
||||||
|
let Some(desired) = desired else {
|
||||||
|
tracing::info!("reconnect supervisor: desired_relay_addr cleared, exiting");
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Has something else already re-registered us (manual
|
||||||
|
// register_signal won the race)? If so, exit.
|
||||||
|
if transport_is_some {
|
||||||
|
tracing::info!("reconnect supervisor: transport already set by another path, exiting");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has the desired relay changed under us? Switch to the new one.
|
||||||
|
if desired != current_relay {
|
||||||
|
tracing::info!(old = %current_relay, new = %desired, "reconnect supervisor: desired relay changed");
|
||||||
|
current_relay = desired.clone();
|
||||||
|
attempt = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back off before the retry (skip on attempt 0 so the first
|
||||||
|
// reconnect kicks in fast).
|
||||||
|
if attempt > 0 {
|
||||||
|
let idx = (attempt - 1).min(backoff_schedule_ms.len() - 1);
|
||||||
|
let wait_ms = backoff_schedule_ms[idx];
|
||||||
|
tracing::info!(
|
||||||
|
attempt,
|
||||||
|
wait_ms,
|
||||||
|
relay = %current_relay,
|
||||||
|
"reconnect supervisor: backing off"
|
||||||
|
);
|
||||||
|
emit_call_debug(
|
||||||
|
&app,
|
||||||
|
"signal:reconnect_backoff",
|
||||||
|
serde_json::json!({ "attempt": attempt, "wait_ms": wait_ms, "relay": current_relay }),
|
||||||
|
);
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(wait_ms)).await;
|
||||||
|
}
|
||||||
|
attempt += 1;
|
||||||
|
|
||||||
|
// One-shot attempt. do_register_signal will set the
|
||||||
|
// transport + spawn a fresh recv loop on success.
|
||||||
|
//
|
||||||
|
// CRITICAL: release our single-flight guard BEFORE
|
||||||
|
// do_register_signal spawns the new recv loop, because that
|
||||||
|
// recv loop's exit path also checks `reconnect_in_progress`
|
||||||
|
// to decide whether to spawn a supervisor of its own. If we
|
||||||
|
// held it here and later exited, the slot would be released
|
||||||
|
// too late for the next drop to trigger a fresh supervisor.
|
||||||
|
{
|
||||||
|
let mut sig = signal_state.lock().await;
|
||||||
|
sig.reconnect_in_progress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit_call_debug(
|
||||||
|
&app,
|
||||||
|
"signal:reconnect_attempt",
|
||||||
|
serde_json::json!({ "attempt": attempt, "relay": current_relay }),
|
||||||
|
);
|
||||||
|
match do_register_signal(signal_state.clone(), app.clone(), current_relay.clone()).await {
|
||||||
|
Ok(fp) => {
|
||||||
|
tracing::info!(%fp, relay = %current_relay, "reconnect supervisor: success");
|
||||||
|
emit_call_debug(
|
||||||
|
&app,
|
||||||
|
"signal:reconnect_ok",
|
||||||
|
serde_json::json!({ "fingerprint": fp, "relay": current_relay }),
|
||||||
|
);
|
||||||
|
return; // recv loop now owns the connection
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(error = %e, relay = %current_relay, "reconnect supervisor: attempt failed");
|
||||||
|
emit_call_debug(
|
||||||
|
&app,
|
||||||
|
"signal:reconnect_failed",
|
||||||
|
serde_json::json!({ "attempt": attempt, "error": e, "relay": current_relay }),
|
||||||
|
);
|
||||||
|
// Re-claim the single-flight slot for the next iteration.
|
||||||
|
let mut sig = signal_state.lock().await;
|
||||||
|
sig.reconnect_in_progress = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop exited — clean up the slot if we still hold it.
|
||||||
|
let mut sig = signal_state.lock().await;
|
||||||
|
sig.reconnect_in_progress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -1162,19 +1423,13 @@ async fn get_signal_status(state: tauri::State<'_, Arc<AppState>>) -> Result<ser
|
|||||||
|
|
||||||
/// Tear down the signal connection so the user goes back to idle. Called
|
/// Tear down the signal connection so the user goes back to idle. Called
|
||||||
/// when the user clicks "Deregister" on the direct-call screen. The
|
/// when the user clicks "Deregister" on the direct-call screen. The
|
||||||
/// spawned recv loop will break out naturally when the transport closes.
|
/// spawned recv loop will break out naturally when the transport closes,
|
||||||
|
/// AND — critically — clearing `desired_relay_addr` here tells that
|
||||||
|
/// exit path NOT to spawn a reconnect supervisor.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn deregister(state: tauri::State<'_, Arc<AppState>>) -> Result<(), String> {
|
async fn deregister(state: tauri::State<'_, Arc<AppState>>) -> Result<(), String> {
|
||||||
let mut sig = state.signal.lock().await;
|
internal_deregister(&state.signal, /*keep_desired=*/ false).await;
|
||||||
if let Some(transport) = sig.transport.take() {
|
tracing::info!("deregister: user-requested, desired_relay_addr cleared");
|
||||||
tracing::info!("deregister: closing signal transport");
|
|
||||||
transport.close().await.ok();
|
|
||||||
}
|
|
||||||
sig.endpoint = None;
|
|
||||||
sig.signal_status = "idle".into();
|
|
||||||
sig.incoming_call_id = None;
|
|
||||||
sig.incoming_caller_fp = None;
|
|
||||||
sig.incoming_caller_alias = None;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1192,11 +1447,14 @@ pub fn run() {
|
|||||||
incoming_call_id: None, incoming_caller_fp: None, incoming_caller_alias: None,
|
incoming_call_id: None, incoming_caller_fp: None, incoming_caller_alias: None,
|
||||||
pending_reflect: None,
|
pending_reflect: None,
|
||||||
own_reflex_addr: None,
|
own_reflex_addr: None,
|
||||||
|
desired_relay_addr: None,
|
||||||
|
reconnect_in_progress: false,
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
|
.plugin(tauri_plugin_notification::init())
|
||||||
.manage(state)
|
.manage(state)
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
// Resolve the platform-correct app data dir once at startup so
|
// Resolve the platform-correct app data dir once at startup so
|
||||||
|
|||||||
@@ -2,6 +2,125 @@ import { invoke } from "@tauri-apps/api/core";
|
|||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { generateIdenticon, createIdenticonEl } from "./identicon";
|
import { generateIdenticon, createIdenticonEl } from "./identicon";
|
||||||
|
|
||||||
|
// ── Incoming-call ringer ─────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Web Audio synthesized two-tone ring that loops until stop() is
|
||||||
|
// called. No external asset file — works immediately on every
|
||||||
|
// platform Tauri has a WebView on (Android, macOS, Windows, Linux).
|
||||||
|
//
|
||||||
|
// The pattern is a classic North American ring cadence: 440Hz +
|
||||||
|
// 480Hz tone for 2s, 4s silence, repeat. Volume ramps to ~30%
|
||||||
|
// peak so it's audible without being obnoxious on laptop
|
||||||
|
// speakers. Stops cleanly on stop() — cancels the timer AND
|
||||||
|
// disconnects the active oscillators so there's no tail audio.
|
||||||
|
class Ringer {
|
||||||
|
private ctx: AudioContext | null = null;
|
||||||
|
private timer: number | null = null;
|
||||||
|
private activeNodes: AudioNode[] = [];
|
||||||
|
private running = false;
|
||||||
|
|
||||||
|
start() {
|
||||||
|
if (this.running) return;
|
||||||
|
this.running = true;
|
||||||
|
// Construct the AudioContext lazily on the first ring — some
|
||||||
|
// platforms (iOS WebView, Android WebView) refuse to create
|
||||||
|
// one until after a user gesture, so we MUST be past that
|
||||||
|
// point by the time start() is called. Incoming call event is
|
||||||
|
// user-adjacent enough that the WebView normally allows it.
|
||||||
|
try {
|
||||||
|
if (!this.ctx) {
|
||||||
|
this.ctx = new (window.AudioContext || (window as any).webkitAudioContext)();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Ringer: AudioContext unavailable", e);
|
||||||
|
this.running = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.playOnce();
|
||||||
|
// 2s tone + 4s silence = 6s cadence. Loop with setInterval.
|
||||||
|
this.timer = window.setInterval(() => this.playOnce(), 6000);
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.running = false;
|
||||||
|
if (this.timer != null) {
|
||||||
|
window.clearInterval(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
|
for (const n of this.activeNodes) {
|
||||||
|
try {
|
||||||
|
(n as any).disconnect();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
this.activeNodes = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private playOnce() {
|
||||||
|
if (!this.ctx || !this.running) return;
|
||||||
|
const ctx = this.ctx;
|
||||||
|
const now = ctx.currentTime;
|
||||||
|
const toneDurSec = 2.0;
|
||||||
|
// Two-tone ring: 440Hz (A4) + 480Hz (close to B4). Mix both
|
||||||
|
// through one gain node for envelope control.
|
||||||
|
const gain = ctx.createGain();
|
||||||
|
gain.gain.setValueAtTime(0, now);
|
||||||
|
gain.gain.linearRampToValueAtTime(0.3, now + 0.05);
|
||||||
|
gain.gain.setValueAtTime(0.3, now + toneDurSec - 0.05);
|
||||||
|
gain.gain.linearRampToValueAtTime(0, now + toneDurSec);
|
||||||
|
gain.connect(ctx.destination);
|
||||||
|
|
||||||
|
for (const freq of [440, 480]) {
|
||||||
|
const osc = ctx.createOscillator();
|
||||||
|
osc.type = "sine";
|
||||||
|
osc.frequency.value = freq;
|
||||||
|
osc.connect(gain);
|
||||||
|
osc.start(now);
|
||||||
|
osc.stop(now + toneDurSec);
|
||||||
|
this.activeNodes.push(osc);
|
||||||
|
}
|
||||||
|
this.activeNodes.push(gain);
|
||||||
|
|
||||||
|
// Schedule a cleanup of old nodes after this tone finishes so
|
||||||
|
// the activeNodes array doesn't grow unbounded across long
|
||||||
|
// rings.
|
||||||
|
window.setTimeout(() => {
|
||||||
|
this.activeNodes = this.activeNodes.filter((n) => n !== gain);
|
||||||
|
}, (toneDurSec + 0.1) * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ringer = new Ringer();
|
||||||
|
|
||||||
|
/// Best-effort system notification via the tauri-plugin-notification
|
||||||
|
/// plugin. Uses raw `invoke` so we don't need to import
|
||||||
|
/// `@tauri-apps/plugin-notification` — just invoke the plugin
|
||||||
|
/// commands directly. Silently no-ops if the plugin isn't
|
||||||
|
/// available or permission is denied.
|
||||||
|
async function notifyIncomingCall(from: string) {
|
||||||
|
try {
|
||||||
|
// Make sure we have permission first. On Android this prompts
|
||||||
|
// the user once; after that it's cached.
|
||||||
|
const granted = await invoke<boolean>(
|
||||||
|
"plugin:notification|is_permission_granted",
|
||||||
|
).catch(() => false);
|
||||||
|
if (!granted) {
|
||||||
|
const result = await invoke<string>(
|
||||||
|
"plugin:notification|request_permission",
|
||||||
|
).catch(() => "denied");
|
||||||
|
if (result !== "granted") return;
|
||||||
|
}
|
||||||
|
await invoke("plugin:notification|notify", {
|
||||||
|
options: {
|
||||||
|
title: "Incoming call",
|
||||||
|
body: `From ${from}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Notification plugin missing or refused — not fatal, the
|
||||||
|
// visible panel + ringer still alert the user.
|
||||||
|
console.debug("notify: plugin unavailable or refused", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── WebView hardening ──
|
// ── WebView hardening ──
|
||||||
// Suppress the browser-style right-click context menu on desktop Tauri — it
|
// Suppress the browser-style right-click context menu on desktop Tauri — it
|
||||||
// exposes Inspect/Reload/Back/Forward entries that don't belong in a native-
|
// exposes Inspect/Reload/Back/Forward entries that don't belong in a native-
|
||||||
@@ -347,6 +466,9 @@ function renderRelayDialogList() {
|
|||||||
|
|
||||||
// Click to select
|
// Click to select
|
||||||
item.addEventListener("click", () => {
|
item.addEventListener("click", () => {
|
||||||
|
const prev = loadSettings();
|
||||||
|
const prevRelayAddr = prev.relays[prev.selectedRelay]?.address;
|
||||||
|
|
||||||
const s = loadSettings();
|
const s = loadSettings();
|
||||||
s.selectedRelay = i;
|
s.selectedRelay = i;
|
||||||
|
|
||||||
@@ -358,6 +480,30 @@ function renderRelayDialogList() {
|
|||||||
saveSettingsObj(s);
|
saveSettingsObj(s);
|
||||||
renderRelayDialogList();
|
renderRelayDialogList();
|
||||||
renderRelayButton();
|
renderRelayButton();
|
||||||
|
|
||||||
|
// If the user switched relays and we're currently registered,
|
||||||
|
// transparently re-register against the new one. The Rust
|
||||||
|
// `register_signal` command is idempotent and handles the
|
||||||
|
// swap internally (close old transport → connect new). This
|
||||||
|
// makes "change server" a single-click operation instead of
|
||||||
|
// manual deregister + re-register.
|
||||||
|
const newRelayAddr = r.address;
|
||||||
|
if (newRelayAddr && newRelayAddr !== prevRelayAddr) {
|
||||||
|
(async () => {
|
||||||
|
// Is a signal currently registered? get_signal_status is
|
||||||
|
// cheap and lets us decide whether to kick the swap.
|
||||||
|
try {
|
||||||
|
const st: any = await invoke("get_signal_status");
|
||||||
|
if (st && st.status === "registered") {
|
||||||
|
await invoke<string>("register_signal", { relay: newRelayAddr });
|
||||||
|
// `signal-event { type: "registered" }` from Rust will
|
||||||
|
// update directRegistered for us — no manual render here.
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("relay swap: failed to re-register", e);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
relayDialogList.appendChild(item);
|
relayDialogList.appendChild(item);
|
||||||
@@ -826,9 +972,18 @@ settingsBtnCall.addEventListener("click", openSettings);
|
|||||||
// shows its working state inline so the user knows it's waiting on
|
// shows its working state inline so the user knows it's waiting on
|
||||||
// the relay rather than the network.
|
// the relay rather than the network.
|
||||||
// Phase 2 multi-relay NAT type detection. Probes every configured
|
// Phase 2 multi-relay NAT type detection. Probes every configured
|
||||||
// relay in parallel through transient QUIC connections and
|
// relay in parallel and classifies the result.
|
||||||
// classifies the result. Green = Cone (P2P viable),
|
//
|
||||||
// amber = SymmetricPort (must relay), gray = Multiple / Unknown.
|
// Cone = P2P direct path viable, green cue
|
||||||
|
// SymmetricPort = per-destination port mapping, informational
|
||||||
|
// (P2P will fall back to relay but calls still work)
|
||||||
|
// Multiple = classifier saw different public IPs; informational
|
||||||
|
// Unknown = not enough public probes, neutral
|
||||||
|
//
|
||||||
|
// The classifier drops LAN / private / CGNAT reflex addrs before
|
||||||
|
// deciding, so a mixed "LAN relay + internet relay" setup does NOT
|
||||||
|
// falsely flag as symmetric. Failed probes are shown in the list
|
||||||
|
// for transparency but dimmed, not highlighted.
|
||||||
sNatDetectBtn.addEventListener("click", async () => {
|
sNatDetectBtn.addEventListener("click", async () => {
|
||||||
const s = loadSettings();
|
const s = loadSettings();
|
||||||
if (!s.relays || s.relays.length === 0) {
|
if (!s.relays || s.relays.length === 0) {
|
||||||
@@ -859,17 +1014,18 @@ sNatDetectBtn.addEventListener("click", async () => {
|
|||||||
detection.nat_type === "Cone"
|
detection.nat_type === "Cone"
|
||||||
? `✓ Cone NAT — P2P viable (${detection.consensus_addr})`
|
? `✓ Cone NAT — P2P viable (${detection.consensus_addr})`
|
||||||
: detection.nat_type === "SymmetricPort"
|
: detection.nat_type === "SymmetricPort"
|
||||||
? "⚠ Symmetric NAT — must use relay"
|
? "ℹ Symmetric NAT — P2P falls back to relay, calls still work"
|
||||||
: detection.nat_type === "Multiple"
|
: detection.nat_type === "Multiple"
|
||||||
? "⚠ Multiple IPs — treating as symmetric"
|
? "ℹ Multiple public IPs observed"
|
||||||
: "? Unknown (not enough successful probes)";
|
: "? Unknown (not enough public probes)";
|
||||||
|
|
||||||
|
// Only Cone is "good news green". Everything else is neutral
|
||||||
|
// informational — the user has configured relays so any
|
||||||
|
// classification result just describes their network; none
|
||||||
|
// are "wrong" per se.
|
||||||
const verdictColor =
|
const verdictColor =
|
||||||
detection.nat_type === "Cone"
|
detection.nat_type === "Cone"
|
||||||
? "var(--green)"
|
? "var(--green)"
|
||||||
: detection.nat_type === "SymmetricPort" ||
|
|
||||||
detection.nat_type === "Multiple"
|
|
||||||
? "var(--yellow)"
|
|
||||||
: "var(--text-dim)";
|
: "var(--text-dim)";
|
||||||
|
|
||||||
sNatType.textContent = verdictLabel;
|
sNatType.textContent = verdictLabel;
|
||||||
@@ -882,7 +1038,10 @@ sNatDetectBtn.addEventListener("click", async () => {
|
|||||||
p.relay_addr
|
p.relay_addr
|
||||||
)}) → ${escapeHtml(p.observed_addr)} [${p.latency_ms ?? "?"}ms]</div>`;
|
)}) → ${escapeHtml(p.observed_addr)} [${p.latency_ms ?? "?"}ms]</div>`;
|
||||||
} else {
|
} else {
|
||||||
return `<div style="color:var(--yellow)">• ${escapeHtml(
|
// Failed probes are dimmed, not highlighted — the classifier
|
||||||
|
// already ignores them, and the user doesn't need to be
|
||||||
|
// alarmed by a momentarily-offline relay.
|
||||||
|
return `<div style="color:var(--text-dim);opacity:0.7">• ${escapeHtml(
|
||||||
p.relay_name
|
p.relay_name
|
||||||
)} (${escapeHtml(p.relay_addr)}) → ${escapeHtml(
|
)} (${escapeHtml(p.relay_addr)}) → ${escapeHtml(
|
||||||
p.error ?? "probe failed"
|
p.error ?? "probe failed"
|
||||||
@@ -1169,6 +1328,7 @@ callBtn.addEventListener("click", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
acceptCallBtn.addEventListener("click", async () => {
|
acceptCallBtn.addEventListener("click", async () => {
|
||||||
|
ringer.stop();
|
||||||
const status = await invoke<any>("get_signal_status");
|
const status = await invoke<any>("get_signal_status");
|
||||||
if (status.incoming_call_id) {
|
if (status.incoming_call_id) {
|
||||||
await invoke("answer_call", { callId: status.incoming_call_id, mode: 2 });
|
await invoke("answer_call", { callId: status.incoming_call_id, mode: 2 });
|
||||||
@@ -1177,6 +1337,7 @@ acceptCallBtn.addEventListener("click", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
rejectCallBtn.addEventListener("click", async () => {
|
rejectCallBtn.addEventListener("click", async () => {
|
||||||
|
ringer.stop();
|
||||||
const status = await invoke<any>("get_signal_status");
|
const status = await invoke<any>("get_signal_status");
|
||||||
if (status.incoming_call_id) {
|
if (status.incoming_call_id) {
|
||||||
await invoke("answer_call", { callId: status.incoming_call_id, mode: 0 });
|
await invoke("answer_call", { callId: status.incoming_call_id, mode: 0 });
|
||||||
@@ -1194,12 +1355,21 @@ listen("signal-event", (event: any) => {
|
|||||||
case "incoming":
|
case "incoming":
|
||||||
incomingCallPanel.classList.remove("hidden");
|
incomingCallPanel.classList.remove("hidden");
|
||||||
incomingCaller.textContent = `From: ${data.caller_alias || data.caller_fp?.substring(0, 16) || "unknown"}`;
|
incomingCaller.textContent = `From: ${data.caller_alias || data.caller_fp?.substring(0, 16) || "unknown"}`;
|
||||||
|
// Start ringing + fire a system notification. Both stop in
|
||||||
|
// the hangup/answered/accepted paths below (and via the
|
||||||
|
// accept/reject button handlers).
|
||||||
|
ringer.start();
|
||||||
|
notifyIncomingCall(
|
||||||
|
data.caller_alias || data.caller_fp?.substring(0, 16) || "unknown",
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case "answered":
|
case "answered":
|
||||||
callStatusText.textContent = `Call answered (${data.mode})`;
|
callStatusText.textContent = `Call answered (${data.mode})`;
|
||||||
|
ringer.stop();
|
||||||
break;
|
break;
|
||||||
case "setup":
|
case "setup":
|
||||||
callStatusText.textContent = "Connecting to media...";
|
callStatusText.textContent = "Connecting to media...";
|
||||||
|
ringer.stop();
|
||||||
// Phase 3 hole-punching: peer_direct_addr carries the OTHER
|
// Phase 3 hole-punching: peer_direct_addr carries the OTHER
|
||||||
// party's reflex addr when both sides advertised one. Forward
|
// party's reflex addr when both sides advertised one. Forward
|
||||||
// to Rust connect() which currently logs it + takes the relay
|
// to Rust connect() which currently logs it + takes the relay
|
||||||
@@ -1221,8 +1391,56 @@ listen("signal-event", (event: any) => {
|
|||||||
})();
|
})();
|
||||||
break;
|
break;
|
||||||
case "hangup":
|
case "hangup":
|
||||||
|
// Peer (or the relay) ended the call. Tear down OUR side
|
||||||
|
// of the media engine and return to the connect screen
|
||||||
|
// automatically — the user shouldn't have to hit End Call
|
||||||
|
// on a call that's already over.
|
||||||
|
//
|
||||||
|
// Scenarios this handles:
|
||||||
|
// * active direct call, peer hung up → disconnect + back
|
||||||
|
// to connect screen
|
||||||
|
// * incoming call was ringing but caller bailed → hide
|
||||||
|
// incoming panel (no engine to disconnect)
|
||||||
|
// * setup failure mid-handshake → same as above
|
||||||
callStatusText.textContent = "";
|
callStatusText.textContent = "";
|
||||||
incomingCallPanel.classList.add("hidden");
|
incomingCallPanel.classList.add("hidden");
|
||||||
|
ringer.stop();
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
// disconnect errors out with "not connected" if there's
|
||||||
|
// no active engine — safe to ignore, we just want to
|
||||||
|
// make sure any engine IS torn down.
|
||||||
|
await invoke("disconnect");
|
||||||
|
} catch {}
|
||||||
|
// Suppress the call-event "disconnected" auto-reconnect
|
||||||
|
// path since this was a peer-initiated hangup, not a
|
||||||
|
// transport drop.
|
||||||
|
userDisconnected = true;
|
||||||
|
if (!callScreen.classList.contains("hidden")) {
|
||||||
|
showConnectScreen();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
break;
|
||||||
|
case "reconnecting":
|
||||||
|
// Signal supervisor is retrying the relay connection. Show
|
||||||
|
// a non-blocking indicator; the user can keep using
|
||||||
|
// everything that doesn't need a live signal.
|
||||||
|
{
|
||||||
|
const relay = typeof data.relay === "string" ? data.relay : "relay";
|
||||||
|
directRegistered.textContent = `🔄 reconnecting to ${relay}…`;
|
||||||
|
directRegistered.style.color = "var(--yellow)";
|
||||||
|
directRegistered.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "registered":
|
||||||
|
// Supervisor (re-)succeeded, or the first register landed.
|
||||||
|
// Clear the banner and show the registered state.
|
||||||
|
{
|
||||||
|
const fp = typeof data.fingerprint === "string" ? data.fingerprint : "";
|
||||||
|
directRegistered.textContent = `✓ registered${fp ? ` (${fp.slice(0, 16)}…)` : ""}`;
|
||||||
|
directRegistered.style.color = "var(--green)";
|
||||||
|
directRegistered.classList.remove("hidden");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user