feat: add phase 4 notifications service and in-app center
This commit is contained in:
parent
512e502f9b
commit
7983cfb405
18 changed files with 1118 additions and 18 deletions
10
package-lock.json
generated
10
package-lock.json
generated
|
|
@ -10,6 +10,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.10.1",
|
"@tauri-apps/api": "^2.10.1",
|
||||||
"@tauri-apps/plugin-dialog": "^2.7.0",
|
"@tauri-apps/plugin-dialog": "^2.7.0",
|
||||||
|
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
|
|
@ -1667,6 +1668,15 @@
|
||||||
"@tauri-apps/api": "^2.10.1"
|
"@tauri-apps/api": "^2.10.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-notification": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.10.1",
|
"@tauri-apps/api": "^2.10.1",
|
||||||
"@tauri-apps/plugin-dialog": "^2.7.0",
|
"@tauri-apps/plugin-dialog": "^2.7.0",
|
||||||
|
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
|
|
|
||||||
486
src-tauri/Cargo.lock
generated
486
src-tauri/Cargo.lock
generated
|
|
@ -94,6 +94,137 @@ version = "1.0.102"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||||
|
|
||||||
|
[[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]]
|
||||||
|
name = "async-trait"
|
||||||
|
version = "0.1.89"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atk"
|
name = "atk"
|
||||||
version = "0.18.2"
|
version = "0.18.2"
|
||||||
|
|
@ -189,6 +320,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"
|
||||||
|
|
@ -381,6 +525,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 = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
|
@ -806,6 +959,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"
|
||||||
|
|
@ -833,6 +1013,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 = "fallible-iterator"
|
name = "fallible-iterator"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|
@ -997,6 +1198,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"
|
||||||
|
|
@ -1429,6 +1643,12 @@ version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||||
|
|
||||||
|
[[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"
|
||||||
|
|
@ -2008,6 +2228,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 = "markup5ever"
|
name = "markup5ever"
|
||||||
version = "0.14.1"
|
version = "0.14.1"
|
||||||
|
|
@ -2172,6 +2404,20 @@ version = "0.1.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
|
||||||
|
|
||||||
|
[[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 = "num-conv"
|
name = "num-conv"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|
@ -2411,11 +2657,22 @@ dependencies = [
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
|
"tauri-plugin-notification",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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 = "pango"
|
name = "pango"
|
||||||
version = "0.18.3"
|
version = "0.18.3"
|
||||||
|
|
@ -2441,6 +2698,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"
|
||||||
|
|
@ -2663,6 +2926,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 = "pkg-config"
|
name = "pkg-config"
|
||||||
version = "0.3.33"
|
version = "0.3.33"
|
||||||
|
|
@ -2677,7 +2951,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",
|
||||||
]
|
]
|
||||||
|
|
@ -2695,6 +2969,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",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustix",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "polyval"
|
name = "polyval"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
|
|
@ -2815,6 +3103,15 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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"
|
||||||
|
|
@ -2870,6 +3167,16 @@ dependencies = [
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha 0.9.0",
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_chacha"
|
name = "rand_chacha"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
|
|
@ -2890,6 +3197,16 @@ dependencies = [
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
|
|
@ -2908,6 +3225,15 @@ dependencies = [
|
||||||
"getrandom 0.2.17",
|
"getrandom 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_hc"
|
name = "rand_hc"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -4032,6 +4358,25 @@ dependencies = [
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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.4",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"time",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-runtime"
|
name = "tauri-runtime"
|
||||||
version = "2.10.1"
|
version = "2.10.1"
|
||||||
|
|
@ -4132,6 +4477,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",
|
||||||
|
"windows-version",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.27.0"
|
version = "3.27.0"
|
||||||
|
|
@ -4464,9 +4821,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-attributes"
|
||||||
|
version = "0.1.31"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-core"
|
name = "tracing-core"
|
||||||
version = "0.1.36"
|
version = "0.1.36"
|
||||||
|
|
@ -4516,6 +4885,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"
|
||||||
|
|
@ -5456,6 +5836,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"
|
||||||
|
|
@ -5658,6 +6041,67 @@ dependencies = [
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[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"
|
||||||
|
|
@ -5743,3 +6187,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",
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ tauri-build = { version = "2", features = [] }
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = [] }
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
|
tauri-plugin-notification = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
"windows": ["main"],
|
"windows": ["main"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"dialog:default"
|
"dialog:default",
|
||||||
|
"notification:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod credential;
|
pub mod credential;
|
||||||
|
pub mod notification;
|
||||||
pub mod orchestrator;
|
pub mod orchestrator;
|
||||||
pub mod poller;
|
pub mod poller;
|
||||||
pub mod project;
|
pub mod project;
|
||||||
|
|
|
||||||
35
src-tauri/src/commands/notification.rs
Normal file
35
src-tauri/src/commands/notification.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::notification::Notification;
|
||||||
|
use crate::AppState;
|
||||||
|
use tauri::State;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn list_notifications(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
project_id: String,
|
||||||
|
unread_only: bool,
|
||||||
|
) -> Result<Vec<Notification>, AppError> {
|
||||||
|
let conn = state.db.lock().map_err(|e| AppError::from(e.to_string()))?;
|
||||||
|
let notifications = Notification::list_by_project(&conn, &project_id, unread_only)?;
|
||||||
|
Ok(notifications)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn mark_notification_read(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
id: String,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
|
let conn = state.db.lock().map_err(|e| AppError::from(e.to_string()))?;
|
||||||
|
Notification::mark_read(&conn, &id)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn mark_all_notifications_read(
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
project_id: String,
|
||||||
|
) -> Result<usize, AppError> {
|
||||||
|
let conn = state.db.lock().map_err(|e| AppError::from(e.to_string()))?;
|
||||||
|
let count = Notification::mark_all_read(&conn, &project_id)?;
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ pub struct AppState {
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.plugin(tauri_plugin_notification::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let db_dir = app.path().app_data_dir()?;
|
let db_dir = app.path().app_data_dir()?;
|
||||||
std::fs::create_dir_all(&db_dir)?;
|
std::fs::create_dir_all(&db_dir)?;
|
||||||
|
|
@ -70,6 +71,9 @@ pub fn run() {
|
||||||
commands::tracker::list_processed_tickets,
|
commands::tracker::list_processed_tickets,
|
||||||
commands::poller::manual_poll,
|
commands::poller::manual_poll,
|
||||||
commands::poller::get_queue_status,
|
commands::poller::get_queue_status,
|
||||||
|
commands::notification::list_notifications,
|
||||||
|
commands::notification::mark_notification_read,
|
||||||
|
commands::notification::mark_all_notifications_read,
|
||||||
commands::orchestrator::get_ticket_result,
|
commands::orchestrator::get_ticket_result,
|
||||||
commands::orchestrator::retry_ticket,
|
commands::orchestrator::retry_ticket,
|
||||||
commands::orchestrator::cancel_ticket,
|
commands::orchestrator::cancel_ticket,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod credential;
|
pub mod credential;
|
||||||
|
pub mod notification;
|
||||||
pub mod project;
|
pub mod project;
|
||||||
pub mod ticket;
|
pub mod ticket;
|
||||||
pub mod tracker;
|
pub mod tracker;
|
||||||
|
|
|
||||||
151
src-tauri/src/models/notification.rs
Normal file
151
src-tauri/src/models/notification.rs
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
use rusqlite::{params, Connection, Result};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Notification {
|
||||||
|
pub id: String,
|
||||||
|
pub project_id: String,
|
||||||
|
pub ticket_id: Option<String>,
|
||||||
|
pub notification_type: String,
|
||||||
|
pub title: String,
|
||||||
|
pub message: String,
|
||||||
|
pub read: bool,
|
||||||
|
pub created_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_row(row: &rusqlite::Row) -> rusqlite::Result<Notification> {
|
||||||
|
let read_int: i32 = row.get(6)?;
|
||||||
|
Ok(Notification {
|
||||||
|
id: row.get(0)?,
|
||||||
|
project_id: row.get(1)?,
|
||||||
|
ticket_id: row.get(2)?,
|
||||||
|
notification_type: row.get(3)?,
|
||||||
|
title: row.get(4)?,
|
||||||
|
message: row.get(5)?,
|
||||||
|
read: read_int != 0,
|
||||||
|
created_at: row.get(7)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Notification {
|
||||||
|
pub fn insert(
|
||||||
|
conn: &Connection,
|
||||||
|
project_id: &str,
|
||||||
|
ticket_id: Option<&str>,
|
||||||
|
notification_type: &str,
|
||||||
|
title: &str,
|
||||||
|
message: &str,
|
||||||
|
) -> Result<Notification> {
|
||||||
|
let id = Uuid::new_v4().to_string();
|
||||||
|
let now = chrono::Utc::now().to_rfc3339();
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO notifications (id, project_id, ticket_id, type, title, message, read, created_at) \
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0, ?7)",
|
||||||
|
params![id, project_id, ticket_id, notification_type, title, message, now],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(Notification {
|
||||||
|
id,
|
||||||
|
project_id: project_id.to_string(),
|
||||||
|
ticket_id: ticket_id.map(String::from),
|
||||||
|
notification_type: notification_type.to_string(),
|
||||||
|
title: title.to_string(),
|
||||||
|
message: message.to_string(),
|
||||||
|
read: false,
|
||||||
|
created_at: now,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_by_project(
|
||||||
|
conn: &Connection,
|
||||||
|
project_id: &str,
|
||||||
|
unread_only: bool,
|
||||||
|
) -> Result<Vec<Notification>> {
|
||||||
|
let sql = if unread_only {
|
||||||
|
"SELECT id, project_id, ticket_id, type, title, message, read, created_at \
|
||||||
|
FROM notifications WHERE project_id = ?1 AND read = 0 ORDER BY created_at DESC"
|
||||||
|
} else {
|
||||||
|
"SELECT id, project_id, ticket_id, type, title, message, read, created_at \
|
||||||
|
FROM notifications WHERE project_id = ?1 ORDER BY created_at DESC"
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut stmt = conn.prepare(sql)?;
|
||||||
|
let rows = stmt.query_map(params![project_id], from_row)?;
|
||||||
|
rows.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mark_read(conn: &Connection, id: &str) -> Result<()> {
|
||||||
|
conn.execute("UPDATE notifications SET read = 1 WHERE id = ?1", params![id])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mark_all_read(conn: &Connection, project_id: &str) -> Result<usize> {
|
||||||
|
let affected = conn.execute(
|
||||||
|
"UPDATE notifications SET read = 1 WHERE project_id = ?1 AND read = 0",
|
||||||
|
params![project_id],
|
||||||
|
)?;
|
||||||
|
Ok(affected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::db;
|
||||||
|
use crate::models::project::Project;
|
||||||
|
|
||||||
|
fn setup() -> (Connection, String) {
|
||||||
|
let conn = db::init_in_memory().expect("db init should succeed");
|
||||||
|
let project = Project::insert(&conn, "P", "/tmp/p", None, "main").unwrap();
|
||||||
|
(conn, project.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_insert_and_list() {
|
||||||
|
let (conn, project_id) = setup();
|
||||||
|
|
||||||
|
let n = Notification::insert(
|
||||||
|
&conn,
|
||||||
|
&project_id,
|
||||||
|
None,
|
||||||
|
"NewTicket",
|
||||||
|
"Ticket #1",
|
||||||
|
"Nouveau ticket detecte",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(n.notification_type, "NewTicket");
|
||||||
|
assert!(!n.read);
|
||||||
|
|
||||||
|
let list = Notification::list_by_project(&conn, &project_id, false).unwrap();
|
||||||
|
assert_eq!(list.len(), 1);
|
||||||
|
assert_eq!(list[0].id, n.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mark_read() {
|
||||||
|
let (conn, project_id) = setup();
|
||||||
|
|
||||||
|
let n = Notification::insert(&conn, &project_id, None, "Error", "Oops", "Erreur").unwrap();
|
||||||
|
Notification::mark_read(&conn, &n.id).unwrap();
|
||||||
|
|
||||||
|
let unread = Notification::list_by_project(&conn, &project_id, true).unwrap();
|
||||||
|
assert_eq!(unread.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_mark_all_read() {
|
||||||
|
let (conn, project_id) = setup();
|
||||||
|
|
||||||
|
Notification::insert(&conn, &project_id, None, "NewTicket", "A", "B").unwrap();
|
||||||
|
Notification::insert(&conn, &project_id, None, "FixReady", "C", "D").unwrap();
|
||||||
|
|
||||||
|
let affected = Notification::mark_all_read(&conn, &project_id).unwrap();
|
||||||
|
assert_eq!(affected, 2);
|
||||||
|
|
||||||
|
let unread = Notification::list_by_project(&conn, &project_id, true).unwrap();
|
||||||
|
assert_eq!(unread.len(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub mod filter_engine;
|
pub mod filter_engine;
|
||||||
|
pub mod notifier;
|
||||||
pub mod orchestrator;
|
pub mod orchestrator;
|
||||||
pub mod poller;
|
pub mod poller;
|
||||||
pub mod tuleap_client;
|
pub mod tuleap_client;
|
||||||
|
|
|
||||||
123
src-tauri/src/services/notifier.rs
Normal file
123
src-tauri/src/services/notifier.rs
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
use crate::models::notification::Notification;
|
||||||
|
use rusqlite::Connection;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tauri::{AppHandle, Emitter};
|
||||||
|
|
||||||
|
pub fn create_and_emit(
|
||||||
|
db: &Arc<Mutex<Connection>>,
|
||||||
|
app_handle: &AppHandle,
|
||||||
|
project_id: &str,
|
||||||
|
ticket_id: Option<&str>,
|
||||||
|
notification_type: &str,
|
||||||
|
title: &str,
|
||||||
|
message: &str,
|
||||||
|
) -> Result<Notification, String> {
|
||||||
|
let notification = {
|
||||||
|
let conn = db.lock().map_err(|e| format!("DB lock failed: {}", e))?;
|
||||||
|
Notification::insert(
|
||||||
|
&conn,
|
||||||
|
project_id,
|
||||||
|
ticket_id,
|
||||||
|
notification_type,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("Failed to insert notification: {}", e))?
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = app_handle.emit(
|
||||||
|
"new-notification",
|
||||||
|
serde_json::json!({
|
||||||
|
"notification": notification,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notify_new_ticket(
|
||||||
|
db: &Arc<Mutex<Connection>>,
|
||||||
|
app_handle: &AppHandle,
|
||||||
|
project_id: &str,
|
||||||
|
ticket_id: &str,
|
||||||
|
artifact_id: i32,
|
||||||
|
artifact_title: &str,
|
||||||
|
) {
|
||||||
|
let title = format!("Nouveau ticket #{}", artifact_id);
|
||||||
|
let message = artifact_title.to_string();
|
||||||
|
let _ = create_and_emit(
|
||||||
|
db,
|
||||||
|
app_handle,
|
||||||
|
project_id,
|
||||||
|
Some(ticket_id),
|
||||||
|
"NewTicket",
|
||||||
|
&title,
|
||||||
|
&message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notify_analysis_done(
|
||||||
|
db: &Arc<Mutex<Connection>>,
|
||||||
|
app_handle: &AppHandle,
|
||||||
|
project_id: &str,
|
||||||
|
ticket_id: &str,
|
||||||
|
artifact_id: i32,
|
||||||
|
) {
|
||||||
|
let title = format!("Analyse terminee #{}", artifact_id);
|
||||||
|
let message = "Aucune correction de code necessaire".to_string();
|
||||||
|
let _ = create_and_emit(
|
||||||
|
db,
|
||||||
|
app_handle,
|
||||||
|
project_id,
|
||||||
|
Some(ticket_id),
|
||||||
|
"AnalysisDone",
|
||||||
|
&title,
|
||||||
|
&message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notify_fix_ready(
|
||||||
|
db: &Arc<Mutex<Connection>>,
|
||||||
|
app_handle: &AppHandle,
|
||||||
|
project_id: &str,
|
||||||
|
ticket_id: &str,
|
||||||
|
artifact_id: i32,
|
||||||
|
) {
|
||||||
|
let title = format!("Fix pret #{}", artifact_id);
|
||||||
|
let message = "Un correctif est disponible pour revue".to_string();
|
||||||
|
let _ = create_and_emit(
|
||||||
|
db,
|
||||||
|
app_handle,
|
||||||
|
project_id,
|
||||||
|
Some(ticket_id),
|
||||||
|
"FixReady",
|
||||||
|
&title,
|
||||||
|
&message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notify_error(
|
||||||
|
db: &Arc<Mutex<Connection>>,
|
||||||
|
app_handle: &AppHandle,
|
||||||
|
project_id: &str,
|
||||||
|
ticket_id: &str,
|
||||||
|
artifact_id: i32,
|
||||||
|
error: &str,
|
||||||
|
) {
|
||||||
|
let title = format!("Erreur ticket #{}", artifact_id);
|
||||||
|
let message = if error.is_empty() {
|
||||||
|
"Erreur lors du traitement du ticket".to_string()
|
||||||
|
} else {
|
||||||
|
error.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = create_and_emit(
|
||||||
|
db,
|
||||||
|
app_handle,
|
||||||
|
project_id,
|
||||||
|
Some(ticket_id),
|
||||||
|
"Error",
|
||||||
|
&title,
|
||||||
|
&message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ use crate::models::project::Project;
|
||||||
use crate::models::ticket::ProcessedTicket;
|
use crate::models::ticket::ProcessedTicket;
|
||||||
use crate::models::tracker::WatchedTracker;
|
use crate::models::tracker::WatchedTracker;
|
||||||
use crate::models::worktree::Worktree;
|
use crate::models::worktree::Worktree;
|
||||||
use crate::services::worktree_manager;
|
use crate::services::{notifier, worktree_manager};
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
|
|
@ -205,6 +205,15 @@ async fn process_ticket(db: &Arc<Mutex<Connection>>, app_handle: &AppHandle) ->
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let conn = db.lock().map_err(|e| format!("DB lock: {}", e))?;
|
let conn = db.lock().map_err(|e| format!("DB lock: {}", e))?;
|
||||||
let _ = ProcessedTicket::set_error(&conn, &ticket.id, &e);
|
let _ = ProcessedTicket::set_error(&conn, &ticket.id, &e);
|
||||||
|
drop(conn);
|
||||||
|
notifier::notify_error(
|
||||||
|
db,
|
||||||
|
app_handle,
|
||||||
|
&project.id,
|
||||||
|
&ticket.id,
|
||||||
|
ticket.artifact_id,
|
||||||
|
&e,
|
||||||
|
);
|
||||||
let _ = app_handle.emit(
|
let _ = app_handle.emit(
|
||||||
"ticket-processing-error",
|
"ticket-processing-error",
|
||||||
serde_json::json!({ "ticket_id": ticket.id, "error": e }),
|
serde_json::json!({ "ticket_id": ticket.id, "error": e }),
|
||||||
|
|
@ -228,6 +237,13 @@ async fn process_ticket(db: &Arc<Mutex<Connection>>, app_handle: &AppHandle) ->
|
||||||
"ticket-processing-done",
|
"ticket-processing-done",
|
||||||
serde_json::json!({ "ticket_id": ticket.id }),
|
serde_json::json!({ "ticket_id": ticket.id }),
|
||||||
);
|
);
|
||||||
|
notifier::notify_analysis_done(
|
||||||
|
db,
|
||||||
|
app_handle,
|
||||||
|
&project.id,
|
||||||
|
&ticket.id,
|
||||||
|
ticket.artifact_id,
|
||||||
|
);
|
||||||
return Ok(true);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,14 +255,28 @@ async fn process_ticket(db: &Arc<Mutex<Connection>>, app_handle: &AppHandle) ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (wt_path, branch_name) =
|
let worktree_result =
|
||||||
worktree_manager::create_worktree(&project.path, &project.base_branch, ticket.artifact_id)
|
worktree_manager::create_worktree(&project.path, &project.base_branch, ticket.artifact_id);
|
||||||
.inspect_err(|e| {
|
|
||||||
let conn = db.lock().ok();
|
if let Err(e) = &worktree_result {
|
||||||
if let Some(conn) = conn {
|
if let Ok(conn) = db.lock() {
|
||||||
let _ = ProcessedTicket::set_error(&conn, &ticket.id, e);
|
let _ = ProcessedTicket::set_error(&conn, &ticket.id, e);
|
||||||
}
|
}
|
||||||
})?;
|
notifier::notify_error(
|
||||||
|
db,
|
||||||
|
app_handle,
|
||||||
|
&project.id,
|
||||||
|
&ticket.id,
|
||||||
|
ticket.artifact_id,
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
let _ = app_handle.emit(
|
||||||
|
"ticket-processing-error",
|
||||||
|
serde_json::json!({ "ticket_id": ticket.id, "error": e }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let (wt_path, branch_name) = worktree_result?;
|
||||||
|
|
||||||
{
|
{
|
||||||
let conn = db.lock().map_err(|e| format!("DB lock: {}", e))?;
|
let conn = db.lock().map_err(|e| format!("DB lock: {}", e))?;
|
||||||
|
|
@ -283,6 +313,15 @@ async fn process_ticket(db: &Arc<Mutex<Connection>>, app_handle: &AppHandle) ->
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let conn = db.lock().map_err(|e2| format!("DB lock: {}", e2))?;
|
let conn = db.lock().map_err(|e2| format!("DB lock: {}", e2))?;
|
||||||
let _ = ProcessedTicket::set_error(&conn, &ticket.id, &e);
|
let _ = ProcessedTicket::set_error(&conn, &ticket.id, &e);
|
||||||
|
drop(conn);
|
||||||
|
notifier::notify_error(
|
||||||
|
db,
|
||||||
|
app_handle,
|
||||||
|
&project.id,
|
||||||
|
&ticket.id,
|
||||||
|
ticket.artifact_id,
|
||||||
|
&e,
|
||||||
|
);
|
||||||
let _ = app_handle.emit(
|
let _ = app_handle.emit(
|
||||||
"ticket-processing-error",
|
"ticket-processing-error",
|
||||||
serde_json::json!({ "ticket_id": ticket.id, "error": e }),
|
serde_json::json!({ "ticket_id": ticket.id, "error": e }),
|
||||||
|
|
@ -303,6 +342,13 @@ async fn process_ticket(db: &Arc<Mutex<Connection>>, app_handle: &AppHandle) ->
|
||||||
"ticket-processing-done",
|
"ticket-processing-done",
|
||||||
serde_json::json!({ "ticket_id": ticket.id }),
|
serde_json::json!({ "ticket_id": ticket.id }),
|
||||||
);
|
);
|
||||||
|
notifier::notify_fix_ready(
|
||||||
|
db,
|
||||||
|
app_handle,
|
||||||
|
&project.id,
|
||||||
|
&ticket.id,
|
||||||
|
ticket.artifact_id,
|
||||||
|
);
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::models::credential::TuleapCredentials;
|
use crate::models::credential::TuleapCredentials;
|
||||||
use crate::models::ticket::ProcessedTicket;
|
use crate::models::ticket::ProcessedTicket;
|
||||||
use crate::models::tracker::WatchedTracker;
|
use crate::models::tracker::WatchedTracker;
|
||||||
use crate::services::{crypto, filter_engine};
|
use crate::services::{crypto, filter_engine, notifier};
|
||||||
use crate::services::tuleap_client::TuleapClient;
|
use crate::services::tuleap_client::TuleapClient;
|
||||||
use rusqlite::Connection;
|
use rusqlite::Connection;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
@ -115,7 +115,7 @@ async fn poll_single_tracker(
|
||||||
let filtered = filter_engine::apply_filters(&artifacts, &tracker.filters);
|
let filtered = filter_engine::apply_filters(&artifacts, &tracker.filters);
|
||||||
|
|
||||||
// 3. Insert new tickets and update last_polled_at
|
// 3. Insert new tickets and update last_polled_at
|
||||||
let new_count = {
|
let new_tickets = {
|
||||||
let conn = match db.lock() {
|
let conn = match db.lock() {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -124,7 +124,7 @@ async fn poll_single_tracker(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut count = 0usize;
|
let mut inserted = Vec::new();
|
||||||
|
|
||||||
for artifact in &filtered {
|
for artifact in &filtered {
|
||||||
let artifact_id = artifact
|
let artifact_id = artifact
|
||||||
|
|
@ -148,7 +148,7 @@ async fn poll_single_tracker(
|
||||||
&artifact_title,
|
&artifact_title,
|
||||||
&artifact_data,
|
&artifact_data,
|
||||||
) {
|
) {
|
||||||
Ok(Some(_)) => count += 1,
|
Ok(Some(ticket)) => inserted.push(ticket),
|
||||||
Ok(None) => {}
|
Ok(None) => {}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("poller: failed to insert ticket (artifact {}): {}", artifact_id, e);
|
eprintln!("poller: failed to insert ticket (artifact {}): {}", artifact_id, e);
|
||||||
|
|
@ -161,20 +161,31 @@ async fn poll_single_tracker(
|
||||||
eprintln!("poller: failed to update last_polled_at for tracker {}: {}", tracker.id, e);
|
eprintln!("poller: failed to update last_polled_at for tracker {}: {}", tracker.id, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
count
|
inserted
|
||||||
}; // lock released
|
}; // lock released
|
||||||
|
|
||||||
// 5. Emit event if new tickets found
|
// 5. Emit event if new tickets found
|
||||||
if new_count > 0 {
|
if !new_tickets.is_empty() {
|
||||||
if let Err(e) = app_handle.emit(
|
if let Err(e) = app_handle.emit(
|
||||||
"new-tickets-detected",
|
"new-tickets-detected",
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"tracker_id": tracker.id,
|
"tracker_id": tracker.id,
|
||||||
"tracker_label": tracker.tracker_label,
|
"tracker_label": tracker.tracker_label,
|
||||||
"count": new_count,
|
"count": new_tickets.len(),
|
||||||
}),
|
}),
|
||||||
) {
|
) {
|
||||||
eprintln!("poller: failed to emit event: {}", e);
|
eprintln!("poller: failed to emit event: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for ticket in &new_tickets {
|
||||||
|
notifier::notify_new_ticket(
|
||||||
|
db,
|
||||||
|
app_handle,
|
||||||
|
&tracker.project_id,
|
||||||
|
&ticket.id,
|
||||||
|
ticket.artifact_id,
|
||||||
|
&ticket.artifact_title,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from "react-router-dom";
|
||||||
|
import NotificationCenter from "./NotificationCenter";
|
||||||
import Sidebar from "./Sidebar";
|
import Sidebar from "./Sidebar";
|
||||||
|
|
||||||
export default function AppLayout() {
|
export default function AppLayout() {
|
||||||
|
|
@ -6,6 +7,11 @@ export default function AppLayout() {
|
||||||
<div className="flex h-screen">
|
<div className="flex h-screen">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<main className="flex-1 overflow-y-auto bg-gray-50">
|
<main className="flex-1 overflow-y-auto bg-gray-50">
|
||||||
|
<header className="sticky top-0 z-10 border-b border-gray-200 bg-gray-50/95 px-6 py-3 backdrop-blur">
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<NotificationCenter />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
198
src/components/layout/NotificationCenter.tsx
Normal file
198
src/components/layout/NotificationCenter.tsx
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
import {
|
||||||
|
isPermissionGranted,
|
||||||
|
requestPermission,
|
||||||
|
sendNotification,
|
||||||
|
} from "@tauri-apps/plugin-notification";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import {
|
||||||
|
listNotifications,
|
||||||
|
markAllNotificationsRead,
|
||||||
|
markNotificationRead,
|
||||||
|
} from "../../lib/api";
|
||||||
|
import type { OrchaiNotification } from "../../lib/types";
|
||||||
|
|
||||||
|
type NewNotificationEvent = {
|
||||||
|
notification: OrchaiNotification;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function showSystemNotification(notification: OrchaiNotification) {
|
||||||
|
try {
|
||||||
|
let permissionGranted = await isPermissionGranted();
|
||||||
|
if (!permissionGranted) {
|
||||||
|
const permission = await requestPermission();
|
||||||
|
permissionGranted = permission === "granted";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissionGranted) {
|
||||||
|
sendNotification({
|
||||||
|
title: notification.title,
|
||||||
|
body: notification.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Best effort only
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NotificationCenter() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { projectId } = useParams();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [notifications, setNotifications] = useState<OrchaiNotification[]>([]);
|
||||||
|
|
||||||
|
async function loadNotifications() {
|
||||||
|
if (!projectId) {
|
||||||
|
setNotifications([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const items = await listNotifications(projectId, false);
|
||||||
|
setNotifications(items);
|
||||||
|
} catch {
|
||||||
|
// Ignore load errors in layout chrome
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadNotifications();
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let unlisten: (() => void) | null = null;
|
||||||
|
|
||||||
|
const setup = async () => {
|
||||||
|
unlisten = await listen<NewNotificationEvent>("new-notification", (event) => {
|
||||||
|
const incoming = event.payload.notification;
|
||||||
|
|
||||||
|
if (projectId && incoming.project_id !== projectId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setNotifications((prev) => {
|
||||||
|
const withoutDuplicate = prev.filter((n) => n.id !== incoming.id);
|
||||||
|
return [incoming, ...withoutDuplicate];
|
||||||
|
});
|
||||||
|
|
||||||
|
void showSystemNotification(incoming);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
void setup();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (unlisten) {
|
||||||
|
unlisten();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
|
const unreadCount = useMemo(
|
||||||
|
() => notifications.filter((n) => !n.read).length,
|
||||||
|
[notifications]
|
||||||
|
);
|
||||||
|
|
||||||
|
async function handleOpenNotification(notification: OrchaiNotification) {
|
||||||
|
if (!notification.read) {
|
||||||
|
try {
|
||||||
|
await markNotificationRead(notification.id);
|
||||||
|
setNotifications((prev) =>
|
||||||
|
prev.map((n) => (n.id === notification.id ? { ...n, read: true } : n))
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
|
||||||
|
if (notification.ticket_id) {
|
||||||
|
navigate(`/tickets/${notification.ticket_id}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(`/projects/${notification.project_id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleMarkAllRead() {
|
||||||
|
if (!projectId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await markAllNotificationsRead(projectId);
|
||||||
|
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setOpen((v) => !v)}
|
||||||
|
className="relative rounded border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
Notifications
|
||||||
|
{unreadCount > 0 && (
|
||||||
|
<span className="ml-2 rounded-full bg-red-600 px-1.5 py-0.5 text-xs font-semibold text-white">
|
||||||
|
{unreadCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{open && (
|
||||||
|
<div className="absolute right-0 z-20 mt-2 w-[360px] rounded-lg border border-gray-200 bg-white shadow-lg">
|
||||||
|
<div className="flex items-center justify-between border-b border-gray-200 px-3 py-2">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-800">Notifications</h3>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleMarkAllRead}
|
||||||
|
className="text-xs text-blue-600 hover:underline"
|
||||||
|
disabled={!projectId || unreadCount === 0}
|
||||||
|
>
|
||||||
|
Mark all read
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-h-[420px] overflow-y-auto">
|
||||||
|
{notifications.length === 0 ? (
|
||||||
|
<div className="px-3 py-6 text-center text-sm text-gray-400">
|
||||||
|
No notifications.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
notifications.map((notification) => (
|
||||||
|
<button
|
||||||
|
key={notification.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => void handleOpenNotification(notification)}
|
||||||
|
className={`block w-full border-b border-gray-100 px-3 py-2 text-left hover:bg-gray-50 ${
|
||||||
|
notification.read ? "bg-white" : "bg-blue-50/40"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="truncate text-sm font-medium text-gray-900">
|
||||||
|
{notification.title}
|
||||||
|
</span>
|
||||||
|
{!notification.read && (
|
||||||
|
<span className="h-2 w-2 shrink-0 rounded-full bg-blue-600" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="mt-0.5 line-clamp-2 text-xs text-gray-600">
|
||||||
|
{notification.message}
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-[11px] text-gray-400">
|
||||||
|
{new Date(notification.created_at).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ import type {
|
||||||
ProcessedTicket,
|
ProcessedTicket,
|
||||||
Worktree,
|
Worktree,
|
||||||
TicketResult,
|
TicketResult,
|
||||||
|
OrchaiNotification,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export async function createProject(
|
export async function createProject(
|
||||||
|
|
@ -112,3 +113,17 @@ export async function deleteWorktreeCmd(worktreeId: string): Promise<void> {
|
||||||
export async function listLocalBranches(projectId: string): Promise<string[]> {
|
export async function listLocalBranches(projectId: string): Promise<string[]> {
|
||||||
return invoke("list_local_branches", { projectId });
|
return invoke("list_local_branches", { projectId });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
export async function listNotifications(
|
||||||
|
projectId: string,
|
||||||
|
unreadOnly: boolean
|
||||||
|
): Promise<OrchaiNotification[]> {
|
||||||
|
return invoke("list_notifications", { projectId, unreadOnly });
|
||||||
|
}
|
||||||
|
export async function markNotificationRead(id: string): Promise<void> {
|
||||||
|
return invoke("mark_notification_read", { id });
|
||||||
|
}
|
||||||
|
export async function markAllNotificationsRead(projectId: string): Promise<number> {
|
||||||
|
return invoke("mark_all_notifications_read", { projectId });
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,3 +85,14 @@ export interface TicketResult {
|
||||||
ticket: ProcessedTicket;
|
ticket: ProcessedTicket;
|
||||||
worktree: Worktree | null;
|
worktree: Worktree | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OrchaiNotification {
|
||||||
|
id: string;
|
||||||
|
project_id: string;
|
||||||
|
ticket_id: string | null;
|
||||||
|
notification_type: string;
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
read: boolean;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue