wip: refactor
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
/src
|
||||
/public
|
||||
40
package.json
40
package.json
@@ -15,42 +15,44 @@
|
||||
"@radix-ui/react-dialog": "^1.1.2",
|
||||
"@radix-ui/react-progress": "^1.1.0",
|
||||
"@radix-ui/react-scroll-area": "^1.2.0",
|
||||
"@tanstack/react-query": "^5.59.8",
|
||||
"@tanstack/react-router": "^1.64.0",
|
||||
"@tauri-apps/api": "2.0.2",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0",
|
||||
"@tauri-apps/plugin-dialog": "2.0.0",
|
||||
"@tauri-apps/plugin-fs": "2.0.0",
|
||||
"@tauri-apps/plugin-notification": "2.0.0",
|
||||
"@tauri-apps/plugin-os": "2.0.0",
|
||||
"@tauri-apps/plugin-process": "2.0.0",
|
||||
"@tauri-apps/plugin-shell": "2.0.0",
|
||||
"@tauri-apps/plugin-updater": "2.0.0",
|
||||
"@tanstack/query-broadcast-client-experimental": "^5.59.17",
|
||||
"@tanstack/query-persist-client-core": "^5.59.17",
|
||||
"@tanstack/react-query": "^5.59.19",
|
||||
"@tanstack/react-router": "^1.78.3",
|
||||
"@tauri-apps/api": "^2.0.3",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.1",
|
||||
"@tauri-apps/plugin-fs": "^2.0.1",
|
||||
"@tauri-apps/plugin-notification": "^2.0.0",
|
||||
"@tauri-apps/plugin-os": "^2.0.0",
|
||||
"@tauri-apps/plugin-process": "^2.0.0",
|
||||
"@tauri-apps/plugin-shell": "^2.0.1",
|
||||
"@tauri-apps/plugin-store": "^2.1.0",
|
||||
"@tauri-apps/plugin-updater": "^2.0.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"lru-cache": "^11.0.1",
|
||||
"minidenticons": "^4.2.1",
|
||||
"nostr-tools": "^2.7.2",
|
||||
"nostr-tools": "^2.10.1",
|
||||
"react": "19.0.0-rc-d025ddd3-20240722",
|
||||
"react-dom": "19.0.0-rc-d025ddd3-20240722",
|
||||
"unique-names-generator": "^4.7.1",
|
||||
"virtua": "^0.35.0"
|
||||
"virtua": "^0.36.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.3",
|
||||
"@tanstack/router-plugin": "^1.64.0",
|
||||
"@tanstack/router-plugin": "^1.78.3",
|
||||
"@tauri-apps/cli": "2.0.2",
|
||||
"@types/react": "npm:types-react@19.0.0-rc.1",
|
||||
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.1",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"@vitejs/plugin-react": "^4.3.3",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"babel-plugin-react-compiler": "0.0.0-experimental-696af53-20240625",
|
||||
"clsx": "^2.1.1",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwind-gradient-mask-image": "^1.2.0",
|
||||
"tailwind-merge": "^2.5.3",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"tailwind-merge": "^2.5.4",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.8",
|
||||
"vite": "^5.4.10",
|
||||
"vite-tsconfig-paths": "^5.0.1"
|
||||
},
|
||||
"overrides": {
|
||||
|
||||
876
pnpm-lock.yaml
generated
876
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
472
src-tauri/Cargo.lock
generated
472
src-tauri/Cargo.lock
generated
@@ -2,6 +2,39 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "COOP"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"border",
|
||||
"futures",
|
||||
"itertools 0.13.0",
|
||||
"keyring",
|
||||
"keyring-search",
|
||||
"nostr-connect",
|
||||
"nostr-sdk",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"specta",
|
||||
"specta-typescript",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-clipboard-manager",
|
||||
"tauri-plugin-decorum",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-os",
|
||||
"tauri-plugin-prevent-default",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-store",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-specta",
|
||||
"tokio",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "Inflector"
|
||||
version = "0.11.4"
|
||||
@@ -109,9 +142,9 @@ checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.3.2"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
|
||||
checksum = "775a8770d29db3dadcb858482cc240af7b2ffde4ac4de67d1d4955728103f0e2"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
@@ -619,7 +652,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "border"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/ahkohd/tauri-toolkit?branch=v2#39952d8694268694836b5d9e5fa8473474fe4c45"
|
||||
source = "git+https://github.com/ahkohd/tauri-toolkit?branch=v2#7f5223abbb672664c5ce8b45f55e71f472d53c17"
|
||||
dependencies = [
|
||||
"cocoa 0.25.0",
|
||||
"color",
|
||||
@@ -935,7 +968,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "color"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/ahkohd/tauri-toolkit?branch=v2#39952d8694268694836b5d9e5fa8473474fe4c45"
|
||||
source = "git+https://github.com/ahkohd/tauri-toolkit?branch=v2#7f5223abbb672664c5ce8b45f55e71f472d53c17"
|
||||
dependencies = [
|
||||
"cocoa 0.25.0",
|
||||
"objc",
|
||||
@@ -973,36 +1006,6 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
|
||||
[[package]]
|
||||
name = "coop"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"border",
|
||||
"futures",
|
||||
"itertools 0.13.0",
|
||||
"keyring",
|
||||
"keyring-search",
|
||||
"nostr-connect",
|
||||
"nostr-sdk",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"specta",
|
||||
"specta-typescript",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-clipboard-manager",
|
||||
"tauri-plugin-decorum",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-notification",
|
||||
"tauri-plugin-os",
|
||||
"tauri-plugin-prevent-default",
|
||||
"tauri-plugin-process",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-specta",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@@ -1256,9 +1259,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.3.2"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
|
||||
checksum = "d475dfebcb4854d596b17b09f477616f80f17a550517f2b3615d8c205d5c802b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2193,9 +2196,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.0"
|
||||
version = "0.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
|
||||
checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
@@ -2454,6 +2457,124 @@ dependencies = [
|
||||
"png",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locid"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locid_transform"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locid",
|
||||
"icu_locid_transform_data",
|
||||
"icu_provider",
|
||||
"tinystr",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locid_transform_data"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"utf16_iter",
|
||||
"utf8_iter",
|
||||
"write16",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
"icu_locid_transform",
|
||||
"icu_properties_data",
|
||||
"icu_provider",
|
||||
"tinystr",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locid",
|
||||
"icu_provider_macros",
|
||||
"stable_deref_trait",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider_macros"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
@@ -2462,12 +2583,23 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.5.0"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
|
||||
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
"idna_adapter",
|
||||
"smallvec",
|
||||
"utf8_iter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna_adapter"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
|
||||
dependencies = [
|
||||
"icu_normalizer",
|
||||
"icu_properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2527,7 +2659,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.0",
|
||||
"hashbrown 0.15.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -2760,7 +2892,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f8fe839464d4e4b37d756d7e910063696af79a7e877282cb1825e4ec5f10833"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"linux-keyutils",
|
||||
"log",
|
||||
"security-framework 2.11.1",
|
||||
"security-framework 3.0.0",
|
||||
@@ -2770,8 +2901,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "keyring-search"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fba83ff0a0efb658afeaaa6de89c7abd3ccd34333f5a36d5dae417334fcd488"
|
||||
source = "git+https://github.com/reyamir/keyring-search#59d54e6a28229f09f87b9b043690ee8a1d63221e"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"lazy_static",
|
||||
@@ -2865,7 +2995,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2918,6 +3048,12 @@ version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
|
||||
|
||||
[[package]]
|
||||
name = "lmdb-master-sys"
|
||||
version = "0.2.4"
|
||||
@@ -2972,7 +3108,7 @@ version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.0",
|
||||
"hashbrown 0.15.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3098,9 +3234,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.15.2"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b18047edf23933de40835403d4b9211ffd1dcc65c0eec569df38a1fb8aebd719"
|
||||
checksum = "fdae9c00e61cc0579bcac625e8ad22104c60548a025bfc972dc83868a28e1484"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"dpi",
|
||||
@@ -3213,7 +3349,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
||||
[[package]]
|
||||
name = "nostr"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b"
|
||||
source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"async-trait",
|
||||
@@ -3244,7 +3380,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-connect"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b"
|
||||
source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"async-utility",
|
||||
@@ -3258,7 +3394,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-database"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b"
|
||||
source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"flatbuffers",
|
||||
@@ -3272,7 +3408,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-lmdb"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b"
|
||||
source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
|
||||
dependencies = [
|
||||
"heed",
|
||||
"nostr",
|
||||
@@ -3285,7 +3421,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-relay-pool"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b"
|
||||
source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"async-wsocket",
|
||||
@@ -3303,7 +3439,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-sdk"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b"
|
||||
source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"atomic-destructor",
|
||||
@@ -3322,7 +3458,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-zapper"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b"
|
||||
source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"nostr",
|
||||
@@ -3342,6 +3478,16 @@ dependencies = [
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
||||
dependencies = [
|
||||
"overload",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num"
|
||||
version = "0.4.3"
|
||||
@@ -3456,7 +3602,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nwc"
|
||||
version = "0.35.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b"
|
||||
source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"async-utility",
|
||||
@@ -3794,6 +3940,12 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "page_size"
|
||||
version = "0.6.0"
|
||||
@@ -4638,9 +4790,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.38"
|
||||
version = "0.38.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a"
|
||||
checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"errno",
|
||||
@@ -5046,6 +5198,15 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared_child"
|
||||
version = "1.0.1"
|
||||
@@ -5338,6 +5499,17 @@ dependencies = [
|
||||
"crossbeam-queue",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.3.2"
|
||||
@@ -5714,6 +5886,22 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-store"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e9a580be53f04bb62422d239aa798e88522877f58a0d4a0e745f030055a51bb4"
|
||||
dependencies = [
|
||||
"dunce",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-updater"
|
||||
version = "2.0.2"
|
||||
@@ -5906,24 +6094,34 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.67"
|
||||
version = "1.0.68"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3c6efbfc763e64eb85c11c25320f0737cb7364c4b6336db90aa9ebe27a0bbd"
|
||||
checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.67"
|
||||
version = "1.0.68"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b607164372e89797d78b8e23a6d67d5d1038c1c65efd52e1389ef8b77caba2a6"
|
||||
checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiff"
|
||||
version = "0.9.1"
|
||||
@@ -5966,6 +6164,16 @@ dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.8.0"
|
||||
@@ -5991,6 +6199,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
@@ -6167,6 +6376,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
|
||||
dependencies = [
|
||||
"nu-ansi-term",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6280,12 +6515,6 @@ dependencies = [
|
||||
"unic-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.13"
|
||||
@@ -6325,9 +6554,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.2"
|
||||
version = "2.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
|
||||
checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
@@ -6353,6 +6582,18 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf16_iter"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.11.0"
|
||||
@@ -6374,6 +6615,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.0"
|
||||
@@ -6704,7 +6951,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7125,6 +7372,18 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "write16"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
|
||||
|
||||
[[package]]
|
||||
name = "wry"
|
||||
version = "0.46.3"
|
||||
@@ -7225,6 +7484,30 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "4.0.1"
|
||||
@@ -7311,12 +7594,55 @@ dependencies = [
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55"
|
||||
dependencies = [
|
||||
"zerofrom-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "2.2.0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "coop"
|
||||
version = "0.1.0"
|
||||
name = "COOP"
|
||||
version = "0.2.0"
|
||||
description = "direct message client for desktop"
|
||||
authors = ["npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445"]
|
||||
repository = "https://github.com/lumehq/coop"
|
||||
@@ -24,6 +24,7 @@ tauri-plugin-updater = "2.0.0"
|
||||
tauri-plugin-process = "2.0.0"
|
||||
tauri-plugin-fs = "2.0.0"
|
||||
tauri-plugin-notification = "2.0.0"
|
||||
tauri-plugin-store = "2.1.0"
|
||||
tauri-plugin-decorum = "1.1.0"
|
||||
tauri-plugin-prevent-default = "^0.4"
|
||||
tauri-specta = { version = "2.0.0-rc", features = ["derive", "typescript"] }
|
||||
@@ -33,16 +34,14 @@ nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
|
||||
|
||||
specta = "^2.0.0-rc.20"
|
||||
specta-typescript = "0.0.7"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
itertools = "0.13.0"
|
||||
futures = "0.3.30"
|
||||
keyring-search = "1.2.0"
|
||||
keyring = { version = "3", features = [
|
||||
"apple-native",
|
||||
"windows-native",
|
||||
"linux-native",
|
||||
] }
|
||||
keyring = { version = "3", features = ["apple-native", "windows-native"] }
|
||||
keyring-search = { git = "https://github.com/reyamir/keyring-search" }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
border = { git = "https://github.com/ahkohd/tauri-toolkit", branch = "v2" }
|
||||
|
||||
@@ -14,11 +14,6 @@
|
||||
"core:resources:default",
|
||||
"core:menu:default",
|
||||
"core:tray:default",
|
||||
"shell:allow-open",
|
||||
"dialog:default",
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-ask",
|
||||
"dialog:allow-message",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-center",
|
||||
"core:window:allow-minimize",
|
||||
@@ -27,14 +22,18 @@
|
||||
"core:window:allow-set-focus",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"shell:allow-open",
|
||||
"dialog:default",
|
||||
"dialog:allow-open",
|
||||
"dialog:allow-ask",
|
||||
"dialog:allow-message",
|
||||
"decorum:allow-show-snap-overlay",
|
||||
"prevent-default:default",
|
||||
"updater:default",
|
||||
"updater:allow-check",
|
||||
"updater:allow-download-and-install",
|
||||
"clipboard-manager:allow-write-text",
|
||||
"clipboard-manager:allow-read-text",
|
||||
"fs:allow-read-file",
|
||||
"notification:default"
|
||||
"notification:default",
|
||||
"store:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
wss://purplepag.es/,
|
||||
wss://directory.yabu.me/,
|
||||
wss://user.kindpag.es/,
|
||||
wss://relay.damus.io/,
|
||||
wss://relay.damus.io,
|
||||
wss://relay.primal.net,
|
||||
wss://nostr.fmt.wiz.biz,
|
||||
wss://directory.yabu.me,
|
||||
wss://purplepag.es,
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
edition = "2021"
|
||||
format_strings = true
|
||||
imports_granularity = "Crate"
|
||||
group_imports = "StdExternalCrate"
|
||||
comment_width = 100
|
||||
format_code_in_doc_comments = true
|
||||
wrap_comments = true
|
||||
use_field_init_shorthand = true
|
||||
use_small_heuristics = "Max"
|
||||
hard_tabs = true
|
||||
ignore = ["target/", "gen/"]
|
||||
@@ -5,429 +5,371 @@ use nostr_sdk::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use std::{collections::HashSet, str::FromStr, time::Duration};
|
||||
use tauri::{Emitter, Manager, State};
|
||||
use tauri_plugin_notification::NotificationExt;
|
||||
use tauri::{Manager, State};
|
||||
|
||||
use crate::Nostr;
|
||||
use crate::{Nostr, SUBSCRIPTION_ID};
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct EventPayload {
|
||||
event: String, // JSON String
|
||||
sender: String,
|
||||
event: String, // JSON String
|
||||
sender: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||
struct Account {
|
||||
password: String,
|
||||
nostr_connect: Option<String>,
|
||||
password: String,
|
||||
nostr_connect: Option<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_metadata(id: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?;
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?;
|
||||
|
||||
let filter = Filter::new().author(public_key).kind(Kind::Metadata).limit(1);
|
||||
let filter = Filter::new()
|
||||
.author(public_key)
|
||||
.kind(Kind::Metadata)
|
||||
.limit(1);
|
||||
|
||||
let events = client.database().query(vec![filter]).await.map_err(|e| e.to_string())?;
|
||||
let events = client
|
||||
.database()
|
||||
.query(vec![filter])
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
match events.first() {
|
||||
Some(event) => match Metadata::from_json(&event.content) {
|
||||
Ok(metadata) => Ok(metadata.as_json()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
},
|
||||
None => Err("Metadata not found".into()),
|
||||
}
|
||||
match events.first() {
|
||||
Some(event) => match Metadata::from_json(&event.content) {
|
||||
Ok(metadata) => Ok(metadata.as_json()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
},
|
||||
None => Err("Metadata not found".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_accounts() -> Vec<String> {
|
||||
let search = Search::new().expect("Unexpected.");
|
||||
let results = search.by_service("Coop Secret Storage");
|
||||
let list = List::list_credentials(&results, Limit::All);
|
||||
let accounts: HashSet<String> =
|
||||
list.split_whitespace().filter(|v| v.starts_with("npub1")).map(String::from).collect();
|
||||
let search = Search::new().expect("Unexpected.");
|
||||
let results = search.by_service("Coop Secret Storage");
|
||||
let list = List::list_credentials(&results, Limit::All);
|
||||
let accounts: HashSet<String> = list
|
||||
.split_whitespace()
|
||||
.filter(|v| v.starts_with("npub1"))
|
||||
.map(String::from)
|
||||
.collect();
|
||||
|
||||
accounts.into_iter().collect()
|
||||
accounts.into_iter().collect()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_current_account(state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
||||
let public_key = signer.get_public_key().await.map_err(|e| e.to_string())?;
|
||||
let bech32 = public_key.to_bech32().map_err(|e| e.to_string())?;
|
||||
let client = &state.client;
|
||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
||||
let public_key = signer.get_public_key().await.map_err(|e| e.to_string())?;
|
||||
let bech32 = public_key.to_bech32().map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(bech32)
|
||||
Ok(bech32)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn create_account(
|
||||
name: String,
|
||||
about: String,
|
||||
picture: String,
|
||||
password: String,
|
||||
state: State<'_, Nostr>,
|
||||
name: String,
|
||||
about: String,
|
||||
picture: String,
|
||||
password: String,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let keys = Keys::generate();
|
||||
let npub = keys.public_key().to_bech32().map_err(|e| e.to_string())?;
|
||||
let secret_key = keys.secret_key();
|
||||
let enc = EncryptedSecretKey::new(secret_key, password, 16, KeySecurity::Medium)
|
||||
.map_err(|err| err.to_string())?;
|
||||
let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?;
|
||||
let client = &state.client;
|
||||
let keys = Keys::generate();
|
||||
let npub = keys.public_key().to_bech32().map_err(|e| e.to_string())?;
|
||||
let secret_key = keys.secret_key();
|
||||
let enc = EncryptedSecretKey::new(secret_key, password, 16, KeySecurity::Medium)
|
||||
.map_err(|err| err.to_string())?;
|
||||
let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?;
|
||||
|
||||
// Save account
|
||||
let keyring = Entry::new("Coop Secret Storage", &npub).map_err(|e| e.to_string())?;
|
||||
let account = Account { password: enc_bech32, nostr_connect: None };
|
||||
let j = serde_json::to_string(&account).map_err(|e| e.to_string())?;
|
||||
let _ = keyring.set_password(&j);
|
||||
// Save account
|
||||
let keyring = Entry::new("Coop Secret Storage", &npub).map_err(|e| e.to_string())?;
|
||||
let account = Account {
|
||||
password: enc_bech32,
|
||||
nostr_connect: None,
|
||||
};
|
||||
let j = serde_json::to_string(&account).map_err(|e| e.to_string())?;
|
||||
let _ = keyring.set_password(&j);
|
||||
|
||||
// Update signer
|
||||
client.set_signer(keys).await;
|
||||
// Update signer
|
||||
client.set_signer(keys).await;
|
||||
|
||||
let mut metadata =
|
||||
Metadata::new().display_name(name.clone()).name(name.to_lowercase()).about(about);
|
||||
let mut metadata = Metadata::new()
|
||||
.display_name(name.clone())
|
||||
.name(name.to_lowercase())
|
||||
.about(about);
|
||||
|
||||
if let Ok(url) = Url::parse(&picture) {
|
||||
metadata = metadata.picture(url)
|
||||
}
|
||||
if let Ok(url) = Url::parse(&picture) {
|
||||
metadata = metadata.picture(url)
|
||||
}
|
||||
|
||||
match client.set_metadata(&metadata).await {
|
||||
Ok(_) => Ok(npub),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
match client.set_metadata(&metadata).await {
|
||||
Ok(_) => Ok(npub),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn import_account(key: String, password: String) -> Result<String, String> {
|
||||
let (npub, enc_bech32) = match key.starts_with("ncryptsec") {
|
||||
true => {
|
||||
let enc = EncryptedSecretKey::from_bech32(key).map_err(|err| err.to_string())?;
|
||||
let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?;
|
||||
let secret_key = enc.to_secret_key(password).map_err(|err| err.to_string())?;
|
||||
let keys = Keys::new(secret_key);
|
||||
let npub = keys.public_key().to_bech32().unwrap();
|
||||
let (npub, enc_bech32) = match key.starts_with("ncryptsec") {
|
||||
true => {
|
||||
let enc = EncryptedSecretKey::from_bech32(key).map_err(|err| err.to_string())?;
|
||||
let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?;
|
||||
let secret_key = enc.to_secret_key(password).map_err(|err| err.to_string())?;
|
||||
let keys = Keys::new(secret_key);
|
||||
let npub = keys.public_key().to_bech32().unwrap();
|
||||
|
||||
(npub, enc_bech32)
|
||||
}
|
||||
false => {
|
||||
let secret_key = SecretKey::from_bech32(key).map_err(|err| err.to_string())?;
|
||||
let keys = Keys::new(secret_key.clone());
|
||||
let npub = keys.public_key().to_bech32().unwrap();
|
||||
(npub, enc_bech32)
|
||||
}
|
||||
false => {
|
||||
let secret_key = SecretKey::from_bech32(key).map_err(|err| err.to_string())?;
|
||||
let keys = Keys::new(secret_key.clone());
|
||||
let npub = keys.public_key().to_bech32().unwrap();
|
||||
|
||||
let enc = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Medium)
|
||||
.map_err(|err| err.to_string())?;
|
||||
let enc = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Medium)
|
||||
.map_err(|err| err.to_string())?;
|
||||
|
||||
let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?;
|
||||
let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?;
|
||||
|
||||
(npub, enc_bech32)
|
||||
}
|
||||
};
|
||||
(npub, enc_bech32)
|
||||
}
|
||||
};
|
||||
|
||||
let keyring = Entry::new("Coop Secret Storage", &npub).map_err(|e| e.to_string())?;
|
||||
let keyring = Entry::new("Coop Secret Storage", &npub).map_err(|e| e.to_string())?;
|
||||
|
||||
let account = Account { password: enc_bech32, nostr_connect: None };
|
||||
let account = Account {
|
||||
password: enc_bech32,
|
||||
nostr_connect: None,
|
||||
};
|
||||
|
||||
let pwd = serde_json::to_string(&account).map_err(|e| e.to_string())?;
|
||||
keyring.set_password(&pwd).map_err(|e| e.to_string())?;
|
||||
let pwd = serde_json::to_string(&account).map_err(|e| e.to_string())?;
|
||||
keyring.set_password(&pwd).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(npub)
|
||||
Ok(npub)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn connect_account(uri: String, state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let client = &state.client;
|
||||
|
||||
match NostrConnectURI::parse(uri.clone()) {
|
||||
Ok(bunker_uri) => {
|
||||
// Local user
|
||||
let app_keys = Keys::generate();
|
||||
let app_secret = app_keys.secret_key().to_secret_hex();
|
||||
match NostrConnectURI::parse(uri.clone()) {
|
||||
Ok(bunker_uri) => {
|
||||
// Local user
|
||||
let app_keys = Keys::generate();
|
||||
let app_secret = app_keys.secret_key().to_secret_hex();
|
||||
|
||||
// Get remote user
|
||||
let remote_user = bunker_uri.remote_signer_public_key().unwrap();
|
||||
let remote_npub = remote_user.to_bech32().unwrap();
|
||||
// Get remote user
|
||||
let remote_user = bunker_uri.remote_signer_public_key().unwrap();
|
||||
let remote_npub = remote_user.to_bech32().unwrap();
|
||||
|
||||
match NostrConnect::new(bunker_uri, app_keys, Duration::from_secs(120), None) {
|
||||
Ok(signer) => {
|
||||
let mut url = Url::parse(&uri).unwrap();
|
||||
let query: Vec<(String, String)> = url
|
||||
.query_pairs()
|
||||
.filter(|(name, _)| name != "secret")
|
||||
.map(|(name, value)| (name.into_owned(), value.into_owned()))
|
||||
.collect();
|
||||
url.query_pairs_mut().clear().extend_pairs(&query);
|
||||
match NostrConnect::new(bunker_uri, app_keys, Duration::from_secs(120), None) {
|
||||
Ok(signer) => {
|
||||
let mut url = Url::parse(&uri).unwrap();
|
||||
let query: Vec<(String, String)> = url
|
||||
.query_pairs()
|
||||
.filter(|(name, _)| name != "secret")
|
||||
.map(|(name, value)| (name.into_owned(), value.into_owned()))
|
||||
.collect();
|
||||
url.query_pairs_mut().clear().extend_pairs(&query);
|
||||
|
||||
let key = format!("{}_nostrconnect", remote_npub);
|
||||
let keyring = Entry::new("Coop Secret Storage", &key).unwrap();
|
||||
let account =
|
||||
Account { password: app_secret, nostr_connect: Some(url.to_string()) };
|
||||
let j = serde_json::to_string(&account).map_err(|e| e.to_string())?;
|
||||
let _ = keyring.set_password(&j);
|
||||
let key = format!("{}_nostrconnect", remote_npub);
|
||||
let keyring = Entry::new("Coop Secret Storage", &key).unwrap();
|
||||
let account = Account {
|
||||
password: app_secret,
|
||||
nostr_connect: Some(url.to_string()),
|
||||
};
|
||||
let j = serde_json::to_string(&account).map_err(|e| e.to_string())?;
|
||||
let _ = keyring.set_password(&j);
|
||||
|
||||
// Update signer
|
||||
let _ = client.set_signer(signer).await;
|
||||
// Update signer
|
||||
let _ = client.set_signer(signer).await;
|
||||
|
||||
Ok(remote_npub)
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
Ok(remote_npub)
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn reset_password(key: String, password: String) -> Result<(), String> {
|
||||
let secret_key = SecretKey::from_bech32(key).map_err(|err| err.to_string())?;
|
||||
let keys = Keys::new(secret_key.clone());
|
||||
let npub = keys.public_key().to_bech32().unwrap();
|
||||
let secret_key = SecretKey::from_bech32(key).map_err(|err| err.to_string())?;
|
||||
let keys = Keys::new(secret_key.clone());
|
||||
let npub = keys.public_key().to_bech32().unwrap();
|
||||
|
||||
let enc = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Medium)
|
||||
.map_err(|err| err.to_string())?;
|
||||
let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?;
|
||||
let enc = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Medium)
|
||||
.map_err(|err| err.to_string())?;
|
||||
let enc_bech32 = enc.to_bech32().map_err(|err| err.to_string())?;
|
||||
|
||||
let keyring = Entry::new("Coop Secret Storage", &npub).map_err(|e| e.to_string())?;
|
||||
let account = Account { password: enc_bech32, nostr_connect: None };
|
||||
let j = serde_json::to_string(&account).map_err(|e| e.to_string())?;
|
||||
let _ = keyring.set_password(&j);
|
||||
let keyring = Entry::new("Coop Secret Storage", &npub).map_err(|e| e.to_string())?;
|
||||
let account = Account {
|
||||
password: enc_bech32,
|
||||
nostr_connect: None,
|
||||
};
|
||||
let j = serde_json::to_string(&account).map_err(|e| e.to_string())?;
|
||||
let _ = keyring.set_password(&j);
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn delete_account(id: String) -> Result<(), String> {
|
||||
let keyring = Entry::new("Coop Secret Storage", &id).map_err(|e| e.to_string())?;
|
||||
let _ = keyring.delete_credential();
|
||||
let keyring = Entry::new("Coop Secret Storage", &id).map_err(|e| e.to_string())?;
|
||||
let _ = keyring.delete_credential();
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
||||
let client = &state.client;
|
||||
let client = &state.client;
|
||||
|
||||
match client.get_contact_list(Some(Duration::from_secs(10))).await {
|
||||
Ok(contacts) => {
|
||||
let list = contacts.into_iter().map(|c| c.public_key.to_hex()).collect::<Vec<_>>();
|
||||
Ok(list)
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
match client.get_contact_list(Some(Duration::from_secs(10))).await {
|
||||
Ok(contacts) => {
|
||||
let list = contacts
|
||||
.into_iter()
|
||||
.map(|c| c.public_key.to_hex())
|
||||
.collect::<Vec<_>>();
|
||||
Ok(list)
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn login(
|
||||
account: String,
|
||||
password: String,
|
||||
state: State<'_, Nostr>,
|
||||
handle: tauri::AppHandle,
|
||||
account: String,
|
||||
password: String,
|
||||
state: State<'_, Nostr>,
|
||||
handle: tauri::AppHandle,
|
||||
) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let keyring = Entry::new("Coop Secret Storage", &account).map_err(|e| e.to_string())?;
|
||||
let client = &state.client;
|
||||
let keyring = Entry::new("Coop Secret Storage", &account).map_err(|e| e.to_string())?;
|
||||
|
||||
let account = match keyring.get_password() {
|
||||
Ok(pw) => {
|
||||
let account: Account = serde_json::from_str(&pw).map_err(|e| e.to_string())?;
|
||||
account
|
||||
}
|
||||
Err(e) => return Err(e.to_string()),
|
||||
};
|
||||
let account = match keyring.get_password() {
|
||||
Ok(pw) => {
|
||||
let account: Account = serde_json::from_str(&pw).map_err(|e| e.to_string())?;
|
||||
account
|
||||
}
|
||||
Err(e) => return Err(e.to_string()),
|
||||
};
|
||||
|
||||
let public_key = match account.nostr_connect {
|
||||
None => {
|
||||
let ncryptsec =
|
||||
EncryptedSecretKey::from_bech32(account.password).map_err(|e| e.to_string())?;
|
||||
let secret_key = ncryptsec.to_secret_key(password).map_err(|_| "Wrong password.")?;
|
||||
let keys = Keys::new(secret_key);
|
||||
let public_key = keys.public_key();
|
||||
let public_key = match account.nostr_connect {
|
||||
None => {
|
||||
let ncryptsec =
|
||||
EncryptedSecretKey::from_bech32(account.password).map_err(|e| e.to_string())?;
|
||||
let secret_key = ncryptsec
|
||||
.to_secret_key(password)
|
||||
.map_err(|_| "Wrong password.")?;
|
||||
let keys = Keys::new(secret_key);
|
||||
let public_key = keys.public_key();
|
||||
|
||||
// Update signer
|
||||
client.set_signer(keys).await;
|
||||
// Update signer
|
||||
client.set_signer(keys).await;
|
||||
|
||||
public_key
|
||||
}
|
||||
Some(bunker) => {
|
||||
let uri = NostrConnectURI::parse(bunker).map_err(|e| e.to_string())?;
|
||||
let public_key = uri.remote_signer_public_key().unwrap().clone();
|
||||
let app_keys = Keys::from_str(&account.password).map_err(|e| e.to_string())?;
|
||||
public_key
|
||||
}
|
||||
Some(bunker) => {
|
||||
let uri = NostrConnectURI::parse(bunker).map_err(|e| e.to_string())?;
|
||||
let public_key = uri.remote_signer_public_key().unwrap().to_owned();
|
||||
let app_keys = Keys::from_str(&account.password).map_err(|e| e.to_string())?;
|
||||
|
||||
match NostrConnect::new(uri, app_keys, Duration::from_secs(120), None) {
|
||||
Ok(signer) => {
|
||||
// Update signer
|
||||
client.set_signer(signer).await;
|
||||
// Return public key
|
||||
public_key
|
||||
}
|
||||
Err(e) => return Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
};
|
||||
match NostrConnect::new(uri, app_keys, Duration::from_secs(120), None) {
|
||||
Ok(signer) => {
|
||||
// Update signer
|
||||
client.set_signer(signer).await;
|
||||
// Return public key
|
||||
public_key
|
||||
}
|
||||
Err(e) => return Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let inbox = Filter::new().kind(Kind::Custom(10050)).author(public_key).limit(1);
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Custom(10050))
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
if let Ok(events) =
|
||||
client.get_events_of(vec![inbox], EventSource::relays(Some(Duration::from_secs(3)))).await
|
||||
{
|
||||
if let Some(event) = events.into_iter().next() {
|
||||
let urls = event
|
||||
.tags
|
||||
.iter()
|
||||
.filter_map(|tag| {
|
||||
if let Some(TagStandard::Relay(relay)) = tag.as_standardized() {
|
||||
Some(relay.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut rx = client
|
||||
.stream_events(vec![filter], Some(Duration::from_secs(3)))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
for url in urls.iter() {
|
||||
if let Err(e) = client.add_relay(url).await {
|
||||
println!("Connect relay failed: {}", e)
|
||||
}
|
||||
}
|
||||
while let Some(event) = rx.next().await {
|
||||
let urls = event
|
||||
.tags
|
||||
.iter()
|
||||
.filter_map(|tag| {
|
||||
if let Some(TagStandard::Relay(relay)) = tag.as_standardized() {
|
||||
Some(relay.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Workaround for https://github.com/rust-nostr/nostr/issues/509
|
||||
// TODO: remove this
|
||||
let _ = client
|
||||
.get_events_from(
|
||||
urls.clone(),
|
||||
vec![Filter::new().kind(Kind::TextNote).limit(0)],
|
||||
Some(Duration::from_secs(5)),
|
||||
)
|
||||
.await;
|
||||
for url in urls.iter() {
|
||||
let _ = client.add_relay(url).await;
|
||||
let _ = client.connect_relay(url).await;
|
||||
}
|
||||
|
||||
let mut inbox_relays = state.inbox_relays.lock().await;
|
||||
inbox_relays.insert(public_key, urls);
|
||||
} else {
|
||||
return Err("404".into());
|
||||
}
|
||||
}
|
||||
let mut inbox_relays = state.inbox_relays.write().await;
|
||||
inbox_relays.insert(public_key, urls);
|
||||
}
|
||||
|
||||
let sub_id = SubscriptionId::new("inbox");
|
||||
let new_message = Filter::new().kind(Kind::GiftWrap).pubkey(public_key).limit(0);
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let state = handle.state::<Nostr>();
|
||||
let client = &state.client;
|
||||
|
||||
if client.subscription(&sub_id).await.is_some() {
|
||||
// Remove old subscriotion
|
||||
client.unsubscribe(sub_id.clone()).await;
|
||||
// Resubscribe new message for current user
|
||||
let _ = client.subscribe_with_id(sub_id.clone(), vec![new_message], None).await;
|
||||
} else {
|
||||
let _ = client.subscribe_with_id(sub_id.clone(), vec![new_message], None).await;
|
||||
}
|
||||
let inbox_relays = state.inbox_relays.read().await;
|
||||
let relays = inbox_relays.get(&public_key).unwrap().to_owned();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let state = handle.state::<Nostr>();
|
||||
let client = &state.client;
|
||||
let sub_id = SubscriptionId::new(SUBSCRIPTION_ID);
|
||||
let new_message = Filter::new()
|
||||
.kind(Kind::GiftWrap)
|
||||
.pubkey(public_key)
|
||||
.limit(0);
|
||||
|
||||
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
||||
if let Err(e) = client
|
||||
.subscribe_with_id_to(&relays, sub_id, vec![new_message], None)
|
||||
.await
|
||||
{
|
||||
println!("Subscribe error: {}", e)
|
||||
};
|
||||
|
||||
// Generate a fake sig for rumor event.
|
||||
// TODO: Find better way to save unsigned event to database.
|
||||
let fake_sig = Signature::from_str("f9e79d141c004977192d05a86f81ec7c585179c371f7350a5412d33575a2a356433f58e405c2296ed273e2fe0aafa25b641e39cc4e1f3f261ebf55bce0cbac83").unwrap();
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::GiftWrap)
|
||||
.pubkey(public_key)
|
||||
.limit(200);
|
||||
|
||||
if let Ok(events) = client
|
||||
.pool()
|
||||
.get_events_of(
|
||||
vec![filter],
|
||||
Duration::from_secs(12),
|
||||
FilterOptions::WaitDurationAfterEOSE(Duration::from_secs(20)),
|
||||
)
|
||||
.await
|
||||
{
|
||||
for event in events.iter() {
|
||||
if let Ok(UnwrappedGift { rumor, .. }) = client.unwrap_gift_wrap(event).await {
|
||||
let rumor_clone = rumor.clone();
|
||||
let ev = Event::new(
|
||||
rumor_clone.id.unwrap(),
|
||||
rumor_clone.pubkey,
|
||||
rumor_clone.created_at,
|
||||
rumor_clone.kind,
|
||||
rumor_clone.tags,
|
||||
rumor_clone.content,
|
||||
fake_sig,
|
||||
);
|
||||
let mut rx = client
|
||||
.stream_events_from(&relays, vec![filter], Some(Duration::from_secs(40)))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if let Err(e) = client.database().save_event(&ev).await {
|
||||
println!("Error: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
handle.emit("synchronized", ()).unwrap();
|
||||
}
|
||||
while let Some(event) = rx.next().await {
|
||||
println!("Event: {}", event.as_json());
|
||||
}
|
||||
|
||||
client
|
||||
.handle_notifications(|notification| async {
|
||||
if let RelayPoolNotification::Message { message, .. } = notification {
|
||||
if let RelayMessage::Event { event, subscription_id, .. } = message {
|
||||
if subscription_id == sub_id && event.kind == Kind::GiftWrap {
|
||||
if let Ok(UnwrappedGift { rumor, sender }) =
|
||||
client.unwrap_gift_wrap(&event).await
|
||||
{
|
||||
let rumor_clone = rumor.clone();
|
||||
let ev = Event::new(
|
||||
rumor_clone.id.unwrap(),
|
||||
rumor_clone.pubkey,
|
||||
rumor_clone.created_at,
|
||||
rumor_clone.kind,
|
||||
rumor_clone.tags,
|
||||
rumor_clone.content,
|
||||
fake_sig,
|
||||
);
|
||||
// handle.emit("synchronized", ()).unwrap();
|
||||
});
|
||||
|
||||
// Save rumor to database to further query
|
||||
if let Err(e) = client.database().save_event(&ev).await {
|
||||
println!("[save event] error: {}", e)
|
||||
}
|
||||
|
||||
// Emit new event to frontend
|
||||
if let Err(e) = handle.emit(
|
||||
"event",
|
||||
EventPayload {
|
||||
event: rumor.as_json(),
|
||||
sender: sender.to_hex(),
|
||||
},
|
||||
) {
|
||||
println!("[emit] error: {}", e)
|
||||
}
|
||||
|
||||
if sender != public_key {
|
||||
if let Some(window) = handle.get_webview_window("main") {
|
||||
if !window.is_focused().unwrap() {
|
||||
if let Err(e) = handle
|
||||
.notification()
|
||||
.builder()
|
||||
.body("You have a new message")
|
||||
.title("Coop")
|
||||
.show()
|
||||
{
|
||||
println!("[notification] error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("relay message: {}", message.as_json())
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
})
|
||||
.await
|
||||
});
|
||||
|
||||
Ok(public_key.to_hex())
|
||||
Ok(public_key.to_hex())
|
||||
}
|
||||
|
||||
@@ -8,89 +8,110 @@ use crate::Nostr;
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_chats(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
||||
let client = &state.client;
|
||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
||||
let public_key = signer.get_public_key().await.map_err(|e| e.to_string())?;
|
||||
let client = &state.client;
|
||||
|
||||
let filter = Filter::new().kind(Kind::PrivateDirectMessage).pubkey(public_key);
|
||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
||||
let public_key = signer.get_public_key().await.map_err(|e| e.to_string())?;
|
||||
|
||||
match client.database().query(vec![filter]).await {
|
||||
Ok(events) => {
|
||||
let ev = events
|
||||
.into_iter()
|
||||
.sorted_by_key(|ev| Reverse(ev.created_at))
|
||||
.filter(|ev| ev.pubkey != public_key)
|
||||
.unique_by(|ev| ev.pubkey)
|
||||
.map(|ev| ev.as_json())
|
||||
.collect::<Vec<_>>();
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::PrivateDirectMessage)
|
||||
.pubkey(public_key);
|
||||
let events = client
|
||||
.database()
|
||||
.query(vec![filter])
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(ev)
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
let data = events
|
||||
.into_iter()
|
||||
.sorted_by_key(|ev| Reverse(ev.created_at))
|
||||
.filter(|ev| ev.pubkey != public_key)
|
||||
.unique_by(|ev| ev.pubkey)
|
||||
.map(|ev| ev.as_json())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_chat_messages(id: String, state: State<'_, Nostr>) -> Result<Vec<String>, String> {
|
||||
let client = &state.client;
|
||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
||||
let client = &state.client;
|
||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
||||
|
||||
let receiver = signer.get_public_key().await.map_err(|e| e.to_string())?;
|
||||
let sender = PublicKey::parse(id).map_err(|e| e.to_string())?;
|
||||
let receiver = signer.get_public_key().await.map_err(|e| e.to_string())?;
|
||||
let sender = PublicKey::parse(id).map_err(|e| e.to_string())?;
|
||||
|
||||
let recv_filter =
|
||||
Filter::new().kind(Kind::PrivateDirectMessage).author(sender).pubkey(receiver);
|
||||
let sender_filter =
|
||||
Filter::new().kind(Kind::PrivateDirectMessage).author(receiver).pubkey(sender);
|
||||
let recv_filter = Filter::new()
|
||||
.kind(Kind::PrivateDirectMessage)
|
||||
.author(sender)
|
||||
.pubkey(receiver);
|
||||
let sender_filter = Filter::new()
|
||||
.kind(Kind::PrivateDirectMessage)
|
||||
.author(receiver)
|
||||
.pubkey(sender);
|
||||
|
||||
match client.database().query(vec![recv_filter, sender_filter]).await {
|
||||
Ok(events) => {
|
||||
let ev = events.into_iter().map(|ev| ev.as_json()).collect::<Vec<_>>();
|
||||
Ok(ev)
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
match client
|
||||
.database()
|
||||
.query(vec![recv_filter, sender_filter])
|
||||
.await
|
||||
{
|
||||
Ok(events) => {
|
||||
let ev = events
|
||||
.into_iter()
|
||||
.map(|ev| ev.as_json())
|
||||
.collect::<Vec<_>>();
|
||||
Ok(ev)
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn send_message(
|
||||
to: String,
|
||||
message: String,
|
||||
state: State<'_, Nostr>,
|
||||
to: String,
|
||||
message: String,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<(), String> {
|
||||
let client = &state.client;
|
||||
let relays = state.inbox_relays.lock().await;
|
||||
let client = &state.client;
|
||||
let relays = state.inbox_relays.read().await;
|
||||
|
||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
||||
let public_key = signer.get_public_key().await.map_err(|e| e.to_string())?;
|
||||
let receiver = PublicKey::parse(&to).map_err(|e| e.to_string())?;
|
||||
let signer = client.signer().await.map_err(|e| e.to_string())?;
|
||||
let public_key = signer.get_public_key().await.map_err(|e| e.to_string())?;
|
||||
let receiver = PublicKey::parse(&to).map_err(|e| e.to_string())?;
|
||||
|
||||
// TODO: Add support reply_to
|
||||
let rumor = EventBuilder::private_msg_rumor(receiver, message, None);
|
||||
// TODO: Add support reply_to
|
||||
let rumor = EventBuilder::private_msg_rumor(receiver, message, None);
|
||||
|
||||
// Get inbox relays per member
|
||||
let outbox_urls = match relays.get(&receiver) {
|
||||
Some(relays) => relays,
|
||||
None => return Err("Receiver didn't have inbox relays to receive message.".into()),
|
||||
};
|
||||
// Get inbox relays for receiver
|
||||
let outbox_urls = match relays.get(&receiver) {
|
||||
Some(relays) => relays,
|
||||
None => return Err("Receiver didn't have inbox relays to receive message.".into()),
|
||||
};
|
||||
|
||||
let inbox_urls = match relays.get(&public_key) {
|
||||
Some(relays) => relays,
|
||||
None => return Err("Please config inbox relays to backup your message.".into()),
|
||||
};
|
||||
// Get inbox relays for current user
|
||||
let inbox_urls = match relays.get(&public_key) {
|
||||
Some(relays) => relays,
|
||||
None => return Err("Please config inbox relays to backup your message.".into()),
|
||||
};
|
||||
|
||||
// Send message to [receiver]
|
||||
match client.gift_wrap_to(outbox_urls, &receiver, rumor.clone(), None).await {
|
||||
Ok(_) => {
|
||||
// Send message to [yourself]
|
||||
if let Err(e) = client.gift_wrap_to(inbox_urls, &public_key, rumor, None).await {
|
||||
return Err(e.to_string());
|
||||
}
|
||||
// Send message to [receiver]
|
||||
match client
|
||||
.gift_wrap_to(outbox_urls, &receiver, rumor.clone(), None)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
// Send message to [yourself]
|
||||
if let Err(e) = client
|
||||
.gift_wrap_to(inbox_urls, &public_key, rumor, None)
|
||||
.await
|
||||
{
|
||||
return Err(e.to_string());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use nostr_sdk::prelude::*;
|
||||
use std::{
|
||||
fs::OpenOptions,
|
||||
io::{self, BufRead, Write},
|
||||
time::Duration,
|
||||
fs::OpenOptions,
|
||||
io::{self, BufRead, Write},
|
||||
time::Duration,
|
||||
};
|
||||
use tauri::{Manager, State};
|
||||
|
||||
@@ -11,177 +11,198 @@ use crate::Nostr;
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn get_bootstrap_relays(app: tauri::AppHandle) -> Result<Vec<String>, String> {
|
||||
let relays_path = app
|
||||
.path()
|
||||
.resolve("resources/relays.txt", tauri::path::BaseDirectory::Resource)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let relays_path = app
|
||||
.path()
|
||||
.resolve("resources/relays.txt", tauri::path::BaseDirectory::Resource)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let file = std::fs::File::open(relays_path).map_err(|e| e.to_string())?;
|
||||
let reader = io::BufReader::new(file);
|
||||
let file = std::fs::File::open(relays_path).map_err(|e| e.to_string())?;
|
||||
let reader = io::BufReader::new(file);
|
||||
|
||||
reader.lines().collect::<Result<Vec<String>, io::Error>>().map_err(|e| e.to_string())
|
||||
reader
|
||||
.lines()
|
||||
.collect::<Result<Vec<String>, io::Error>>()
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn set_bootstrap_relays(relays: String, app: tauri::AppHandle) -> Result<(), String> {
|
||||
let relays_path = app
|
||||
.path()
|
||||
.resolve("resources/relays.txt", tauri::path::BaseDirectory::Resource)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut file = OpenOptions::new().write(true).open(relays_path).map_err(|e| e.to_string())?;
|
||||
let relays_path = app
|
||||
.path()
|
||||
.resolve("resources/relays.txt", tauri::path::BaseDirectory::Resource)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.open(relays_path)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
file.write_all(relays.as_bytes()).map_err(|e| e.to_string())
|
||||
file.write_all(relays.as_bytes()).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_inbox_relays(
|
||||
user_id: String,
|
||||
state: State<'_, Nostr>,
|
||||
user_id: String,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::parse(user_id).map_err(|e| e.to_string())?;
|
||||
let inbox = Filter::new().kind(Kind::Custom(10050)).author(public_key).limit(1);
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::parse(user_id).map_err(|e| e.to_string())?;
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Custom(10050))
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
match client.get_events_of(vec![inbox], EventSource::relays(Some(Duration::from_secs(3)))).await
|
||||
{
|
||||
Ok(events) => {
|
||||
if let Some(event) = events.into_iter().next() {
|
||||
let urls = event
|
||||
.tags
|
||||
.iter()
|
||||
.filter_map(|tag| {
|
||||
if let Some(TagStandard::Relay(relay)) = tag.as_standardized() {
|
||||
Some(relay.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut events = Events::new(&[filter.clone()]);
|
||||
|
||||
Ok(urls)
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
let mut rx = client
|
||||
.stream_events(vec![filter], Some(Duration::from_secs(3)))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
while let Some(event) = rx.next().await {
|
||||
events.insert(event);
|
||||
}
|
||||
|
||||
if let Some(event) = events.first() {
|
||||
let urls = event
|
||||
.tags
|
||||
.iter()
|
||||
.filter_map(|tag| {
|
||||
if let Some(TagStandard::Relay(relay)) = tag.as_standardized() {
|
||||
Some(relay.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(urls)
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn ensure_inbox_relays(
|
||||
user_id: String,
|
||||
state: State<'_, Nostr>,
|
||||
user_id: String,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let public_key = PublicKey::parse(user_id).map_err(|e| e.to_string())?;
|
||||
let relays = state.inbox_relays.lock().await;
|
||||
let public_key = PublicKey::parse(user_id).map_err(|e| e.to_string())?;
|
||||
let relays = state.inbox_relays.read().await;
|
||||
|
||||
match relays.get(&public_key) {
|
||||
Some(relays) => {
|
||||
if relays.is_empty() {
|
||||
Err("404".into())
|
||||
} else {
|
||||
Ok(relays.to_owned())
|
||||
}
|
||||
}
|
||||
None => Err("404".into()),
|
||||
}
|
||||
match relays.get(&public_key) {
|
||||
Some(relays) => {
|
||||
if relays.is_empty() {
|
||||
Err("404".into())
|
||||
} else {
|
||||
Ok(relays.to_owned())
|
||||
}
|
||||
}
|
||||
None => Err("404".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_inbox_relays(relays: Vec<String>, state: State<'_, Nostr>) -> Result<(), String> {
|
||||
let client = &state.client;
|
||||
let client = &state.client;
|
||||
|
||||
let tags = relays.into_iter().map(|t| Tag::custom(TagKind::Relay, vec![t])).collect::<Vec<_>>();
|
||||
let event = EventBuilder::new(Kind::Custom(10050), "", tags);
|
||||
let tags = relays
|
||||
.into_iter()
|
||||
.map(|t| Tag::custom(TagKind::Relay, vec![t]))
|
||||
.collect::<Vec<_>>();
|
||||
let event = EventBuilder::new(Kind::Custom(10050), "", tags);
|
||||
|
||||
match client.send_event_builder(event).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
match client.send_event_builder(event).await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn connect_inbox_relays(
|
||||
user_id: String,
|
||||
ignore_cache: bool,
|
||||
state: State<'_, Nostr>,
|
||||
user_id: String,
|
||||
ignore_cache: bool,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::parse(&user_id).map_err(|e| e.to_string())?;
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::parse(&user_id).map_err(|e| e.to_string())?;
|
||||
|
||||
// let nip65_relays = connect_nip65_relays(public_key, client).await;
|
||||
let mut inbox_relays = state.inbox_relays.lock().await;
|
||||
let mut inbox_relays = state.inbox_relays.write().await;
|
||||
|
||||
if !ignore_cache {
|
||||
if let Some(relays) = inbox_relays.get(&public_key) {
|
||||
for url in relays {
|
||||
if let Ok(relay) = client.relay(url).await {
|
||||
if !relay.is_connected() {
|
||||
if let Err(e) = client.connect_relay(url).await {
|
||||
println!("Connect relay failed: {}", e)
|
||||
}
|
||||
}
|
||||
} else if let Err(e) = client.add_relay(url).await {
|
||||
println!("Connect relay failed: {}", e)
|
||||
}
|
||||
}
|
||||
return Ok(relays.to_owned());
|
||||
};
|
||||
};
|
||||
if !ignore_cache {
|
||||
if let Some(relays) = inbox_relays.get(&public_key) {
|
||||
for url in relays {
|
||||
if let Ok(relay) = client.relay(url).await {
|
||||
if !relay.is_connected() {
|
||||
if let Err(e) = client.connect_relay(url).await {
|
||||
println!("Connect relay failed: {}", e)
|
||||
}
|
||||
}
|
||||
} else if let Err(e) = client.add_relay(url).await {
|
||||
println!("Connect relay failed: {}", e)
|
||||
}
|
||||
}
|
||||
return Ok(relays.to_owned());
|
||||
};
|
||||
};
|
||||
|
||||
let inbox = Filter::new().kind(Kind::Custom(10050)).author(public_key).limit(1);
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Custom(10050))
|
||||
.author(public_key)
|
||||
.limit(1);
|
||||
|
||||
match client.get_events_of(vec![inbox], EventSource::relays(Some(Duration::from_secs(3)))).await
|
||||
{
|
||||
Ok(events) => {
|
||||
let mut relays = Vec::new();
|
||||
let mut events = Events::new(&[filter.clone()]);
|
||||
|
||||
if let Some(event) = events.into_iter().next() {
|
||||
for tag in event.tags.iter() {
|
||||
if let Some(TagStandard::Relay(relay)) = tag.as_standardized() {
|
||||
let url = relay.to_string();
|
||||
let mut rx = client
|
||||
.stream_events(vec![filter], Some(Duration::from_secs(3)))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
if let Err(e) = client.add_relay(&url).await {
|
||||
println!("Connect relay failed: {}", e)
|
||||
};
|
||||
while let Some(event) = rx.next().await {
|
||||
events.insert(event);
|
||||
}
|
||||
|
||||
relays.push(url)
|
||||
}
|
||||
}
|
||||
if let Some(event) = events.first() {
|
||||
let mut relays = Vec::new();
|
||||
|
||||
// Update state
|
||||
inbox_relays.insert(public_key, relays.clone());
|
||||
for tag in event.tags.iter() {
|
||||
if let Some(TagStandard::Relay(relay)) = tag.as_standardized() {
|
||||
let url = relay.to_string();
|
||||
let _ = client.add_relay(&url).await;
|
||||
let _ = client.connect_relay(&url).await;
|
||||
|
||||
// Disconnect user's nip65 relays to save bandwidth
|
||||
// disconnect_nip65_relays(nip65_relays, client).await;
|
||||
}
|
||||
relays.push(url)
|
||||
}
|
||||
}
|
||||
|
||||
Ok(relays)
|
||||
}
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
// Update state
|
||||
inbox_relays.insert(public_key, relays.clone());
|
||||
|
||||
Ok(relays)
|
||||
} else {
|
||||
Err("User's inbox relays not found.".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn disconnect_inbox_relays(
|
||||
user_id: String,
|
||||
state: State<'_, Nostr>,
|
||||
user_id: String,
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<(), String> {
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::parse(&user_id).map_err(|e| e.to_string())?;
|
||||
let inbox_relays = state.inbox_relays.lock().await;
|
||||
let client = &state.client;
|
||||
let public_key = PublicKey::parse(&user_id).map_err(|e| e.to_string())?;
|
||||
let inbox_relays = state.inbox_relays.read().await;
|
||||
|
||||
if let Some(relays) = inbox_relays.get(&public_key) {
|
||||
for relay in relays {
|
||||
let _ = client.disconnect_relay(relay).await;
|
||||
}
|
||||
}
|
||||
if let Some(relays) = inbox_relays.get(&public_key) {
|
||||
for relay in relays {
|
||||
let _ = client.disconnect_relay(relay).await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,60 +5,52 @@ use tauri::{Manager, WebviewWindowBuilder};
|
||||
use tauri_plugin_decorum::WebviewWindowExt;
|
||||
|
||||
pub fn setup_tray_icon(app: &tauri::AppHandle) -> tauri::Result<()> {
|
||||
let tray = app.tray_by_id("main").expect("Error: tray icon not found.");
|
||||
let tray = app.tray_by_id("main").expect("Error: tray icon not found.");
|
||||
|
||||
let menu = tauri::menu::MenuBuilder::new(app)
|
||||
.item(
|
||||
&tauri::menu::MenuItem::with_id(app, "open", "Open Coop", true, None::<&str>).unwrap(),
|
||||
)
|
||||
.item(&tauri::menu::MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).unwrap())
|
||||
.build()
|
||||
.expect("Error: cannot create menu.");
|
||||
let menu = tauri::menu::MenuBuilder::new(app)
|
||||
.item(
|
||||
&tauri::menu::MenuItem::with_id(app, "open", "Open Coop", true, None::<&str>).unwrap(),
|
||||
)
|
||||
.item(&tauri::menu::MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).unwrap())
|
||||
.build()
|
||||
.expect("Error: cannot create menu.");
|
||||
|
||||
if tray.set_menu(Some(menu)).is_err() {
|
||||
panic!("Error: cannot set menu for tray icon.")
|
||||
}
|
||||
if tray.set_menu(Some(menu)).is_err() {
|
||||
panic!("Error: cannot set menu for tray icon.")
|
||||
}
|
||||
|
||||
tray.on_menu_event(move |app, event| match event.id.0.as_str() {
|
||||
"open" => {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
if window.is_visible().unwrap_or_default() {
|
||||
let _ = window.set_focus();
|
||||
} else {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
};
|
||||
} else {
|
||||
let config = app.config().app.windows.first().unwrap();
|
||||
let window =
|
||||
WebviewWindowBuilder::from_config(app, config).unwrap().build().unwrap();
|
||||
tray.on_menu_event(move |app, event| match event.id.0.as_str() {
|
||||
"open" => {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
if window.is_visible().unwrap_or_default() {
|
||||
let _ = window.set_focus();
|
||||
} else {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
};
|
||||
} else {
|
||||
let config = app.config().app.windows.first().unwrap();
|
||||
let window = WebviewWindowBuilder::from_config(app, config)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Set custom decoration
|
||||
#[cfg(target_os = "windows")]
|
||||
window.create_overlay_titlebar().unwrap();
|
||||
// Set custom decoration
|
||||
#[cfg(target_os = "windows")]
|
||||
window.create_overlay_titlebar().unwrap();
|
||||
|
||||
// Set traffic light inset
|
||||
#[cfg(target_os = "macos")]
|
||||
window.set_traffic_lights_inset(12.0, 18.0).unwrap();
|
||||
// Set traffic light inset
|
||||
#[cfg(target_os = "macos")]
|
||||
window.set_traffic_lights_inset(12.0, 18.0).unwrap();
|
||||
|
||||
// Workaround for reset traffic light when theme changed
|
||||
#[cfg(target_os = "macos")]
|
||||
let win_ = window.clone();
|
||||
#[cfg(target_os = "macos")]
|
||||
window.on_window_event(move |event| {
|
||||
if let tauri::WindowEvent::ThemeChanged(_) = event {
|
||||
win_.set_traffic_lights_inset(12.0, 18.0).unwrap();
|
||||
}
|
||||
});
|
||||
// Restore native border
|
||||
#[cfg(target_os = "macos")]
|
||||
window.add_border(None);
|
||||
}
|
||||
}
|
||||
"quit" => std::process::exit(0),
|
||||
_ => {}
|
||||
});
|
||||
|
||||
// Restore native border
|
||||
#[cfg(target_os = "macos")]
|
||||
window.add_border(None);
|
||||
}
|
||||
}
|
||||
"quit" => std::process::exit(0),
|
||||
_ => {}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,65 +3,91 @@
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use border::WebviewWindowExt as WebviewWindowExtAlt;
|
||||
use commands::tray::setup_tray_icon;
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta_typescript::Typescript;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env, fs,
|
||||
io::{self, BufRead},
|
||||
str::FromStr,
|
||||
collections::{HashMap, HashSet},
|
||||
env, fs,
|
||||
io::{self, BufRead},
|
||||
str::FromStr,
|
||||
};
|
||||
use tauri::{async_runtime::Mutex, Manager};
|
||||
use tauri::{Emitter, Listener, Manager};
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
use tauri_plugin_decorum::WebviewWindowExt;
|
||||
use tauri_specta::{collect_commands, Builder};
|
||||
use tokio::{sync::RwLock, time::sleep, time::Duration};
|
||||
|
||||
use commands::{account::*, chat::*, relay::*};
|
||||
use commands::{account::*, chat::*, relay::*, tray::*};
|
||||
|
||||
mod commands;
|
||||
|
||||
pub struct Nostr {
|
||||
client: Client,
|
||||
inbox_relays: Mutex<HashMap<PublicKey, Vec<String>>>,
|
||||
client: Client,
|
||||
queue: RwLock<HashSet<PublicKey>>,
|
||||
inbox_relays: RwLock<HashMap<PublicKey, Vec<String>>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Payload {
|
||||
id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct EventPayload {
|
||||
event: String, // JSON String
|
||||
sender: String,
|
||||
}
|
||||
|
||||
pub const QUEUE_DELAY: u64 = 300;
|
||||
pub const SUBSCRIPTION_ID: &str = "inbox";
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_os = "linux")]
|
||||
std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
|
||||
#[cfg(target_os = "linux")]
|
||||
std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
|
||||
|
||||
let builder = Builder::<tauri::Wry>::new().commands(collect_commands![
|
||||
get_bootstrap_relays,
|
||||
set_bootstrap_relays,
|
||||
get_inbox_relays,
|
||||
set_inbox_relays,
|
||||
ensure_inbox_relays,
|
||||
connect_inbox_relays,
|
||||
disconnect_inbox_relays,
|
||||
login,
|
||||
create_account,
|
||||
import_account,
|
||||
connect_account,
|
||||
delete_account,
|
||||
reset_password,
|
||||
get_accounts,
|
||||
get_current_account,
|
||||
get_metadata,
|
||||
get_contact_list,
|
||||
get_chats,
|
||||
get_chat_messages,
|
||||
send_message,
|
||||
]);
|
||||
let builder = Builder::<tauri::Wry>::new().commands(collect_commands![
|
||||
get_bootstrap_relays,
|
||||
set_bootstrap_relays,
|
||||
get_inbox_relays,
|
||||
set_inbox_relays,
|
||||
ensure_inbox_relays,
|
||||
connect_inbox_relays,
|
||||
disconnect_inbox_relays,
|
||||
login,
|
||||
create_account,
|
||||
import_account,
|
||||
connect_account,
|
||||
delete_account,
|
||||
reset_password,
|
||||
get_accounts,
|
||||
get_current_account,
|
||||
get_metadata,
|
||||
get_contact_list,
|
||||
get_chats,
|
||||
get_chat_messages,
|
||||
send_message,
|
||||
]);
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
builder
|
||||
.export(Typescript::default(), "../src/commands.ts")
|
||||
.expect("Failed to export typescript bindings");
|
||||
#[cfg(debug_assertions)]
|
||||
builder
|
||||
.export(Typescript::default(), "../src/commands.ts")
|
||||
.expect("Failed to export typescript bindings");
|
||||
|
||||
tauri::Builder::default()
|
||||
#[cfg(debug_assertions)]
|
||||
let tauri_builder = tauri::Builder::default().plugin(tauri_plugin_devtools::init());
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
let tauri_builder = tauri::Builder::default();
|
||||
|
||||
tauri_builder
|
||||
.invoke_handler(builder.invoke_handler())
|
||||
.setup(move |app| {
|
||||
let handle = app.handle();
|
||||
let handle_clone = handle.clone();
|
||||
let handle_clone_child = handle_clone.clone();
|
||||
|
||||
// Setup tray icon
|
||||
let _ = setup_tray_icon(handle);
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
@@ -88,11 +114,10 @@ fn main() {
|
||||
let _ = fs::create_dir_all(&dir);
|
||||
|
||||
// Setup database
|
||||
let database = NostrLMDB::open(dir.join("nostr-lmdb"))
|
||||
.expect("Error: cannot create database.");
|
||||
let database = NostrLMDB::open(dir.join("nostr-lmdb")).expect("Error: cannot create database.");
|
||||
|
||||
// Setup nostr client
|
||||
let opts = Options::new().gossip(true).autoconnect(true);
|
||||
let opts = Options::new().gossip(true).automatic_authentication(false).max_avg_latency(Duration::from_millis(500));
|
||||
let client = ClientBuilder::default().opts(opts).database(database).build();
|
||||
|
||||
// Add bootstrap relays
|
||||
@@ -123,15 +148,128 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
let _ = client.add_discovery_relay("wss://user.kindpag.es/").await;
|
||||
|
||||
// Connect
|
||||
client.connect().await;
|
||||
|
||||
client
|
||||
});
|
||||
|
||||
// Create global state
|
||||
app.manage(Nostr { client, inbox_relays: Mutex::new(HashMap::new()) });
|
||||
app.manage(Nostr {
|
||||
client,
|
||||
queue: RwLock::new(HashSet::new()),
|
||||
inbox_relays: RwLock::new(HashMap::new()),
|
||||
});
|
||||
|
||||
// Listen for request metadata
|
||||
app.listen_any("request_metadata", move |event| {
|
||||
let payload = event.payload();
|
||||
let parsed_payload: Payload = serde_json::from_str(payload).expect("Parse failed");
|
||||
let handle = handle_clone.to_owned();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let state = handle.state::<Nostr>();
|
||||
let client = &state.client;
|
||||
|
||||
if let Ok(public_key) = PublicKey::parse(parsed_payload.id) {
|
||||
let mut write_queue = state.queue.write().await;
|
||||
write_queue.insert(public_key);
|
||||
};
|
||||
|
||||
// Wait for [QUEUE_DELAY]
|
||||
sleep(Duration::from_millis(QUEUE_DELAY)).await;
|
||||
|
||||
let read_queue = state.queue.read().await;
|
||||
|
||||
if !read_queue.is_empty() {
|
||||
let authors: HashSet<PublicKey> = read_queue.iter().copied().collect();
|
||||
|
||||
let filter = Filter::new().authors(authors).kind(Kind::Metadata).limit(200);
|
||||
|
||||
let opts = SubscribeAutoCloseOptions::default()
|
||||
.filter(FilterOptions::WaitDurationAfterEOSE(Duration::from_secs(2)));
|
||||
|
||||
// Drop queue, you don't need it at this time anymore
|
||||
drop(read_queue);
|
||||
// Clear queue
|
||||
let mut write_queue = state.queue.write().await;
|
||||
write_queue.clear();
|
||||
|
||||
if let Err(e) = client.subscribe(vec![filter], Some(opts)).await {
|
||||
println!("Subscribe error: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Run a thread for handle notification
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let handle = handle_clone_child.to_owned();
|
||||
let state = handle.state::<Nostr>();
|
||||
let client = &state.client;
|
||||
|
||||
// Generate a fake sig for rumor event.
|
||||
// TODO: Find better way to save unsigned event to database.
|
||||
let fake_sig = Signature::from_str("f9e79d141c004977192d05a86f81ec7c585179c371f7350a5412d33575a2a356433f58e405c2296ed273e2fe0aafa25b641e39cc4e1f3f261ebf55bce0cbac83").unwrap();
|
||||
|
||||
let _ = client
|
||||
.handle_notifications(|notification| async {
|
||||
#[allow(clippy::collapsible_match)]
|
||||
if let RelayPoolNotification::Message { message, .. } = notification {
|
||||
if let RelayMessage::Event { event, .. } = message {
|
||||
if event.kind == Kind::GiftWrap {
|
||||
if let Ok(UnwrappedGift { rumor, sender }) =
|
||||
client.unwrap_gift_wrap(&event).await
|
||||
{
|
||||
let mut rumor_clone = rumor.clone();
|
||||
|
||||
// Compute event id if not exist
|
||||
rumor_clone.ensure_id();
|
||||
|
||||
let ev = Event::new(
|
||||
rumor_clone.id.unwrap(), // unwrap() must be fine
|
||||
rumor_clone.pubkey,
|
||||
rumor_clone.created_at,
|
||||
rumor_clone.kind,
|
||||
rumor_clone.tags,
|
||||
rumor_clone.content,
|
||||
fake_sig,
|
||||
);
|
||||
|
||||
// Save rumor to database to further query
|
||||
if let Err(e) = client.database().save_event(&ev).await {
|
||||
println!("[save event] error: {}", e)
|
||||
}
|
||||
|
||||
// Emit new event to frontend
|
||||
if let Err(e) = handle.emit(
|
||||
"event",
|
||||
EventPayload {
|
||||
event: rumor.as_json(),
|
||||
sender: sender.to_hex(),
|
||||
},
|
||||
) {
|
||||
println!("[emit] error: {}", e)
|
||||
}
|
||||
}
|
||||
} else if event.kind == Kind::Metadata {
|
||||
if let Err(e) = handle.emit("metadata", event.as_json()) {
|
||||
println!("Emit error: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
})
|
||||
.await;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.plugin(prevent_default())
|
||||
.plugin(tauri_plugin_store::Builder::default().build())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
@@ -152,14 +290,14 @@ fn main() {
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
fn prevent_default() -> tauri::plugin::TauriPlugin<tauri::Wry> {
|
||||
use tauri_plugin_prevent_default::Flags;
|
||||
use tauri_plugin_prevent_default::Flags;
|
||||
|
||||
tauri_plugin_prevent_default::Builder::new()
|
||||
.with_flags(Flags::all().difference(Flags::CONTEXT_MENU))
|
||||
.build()
|
||||
tauri_plugin_prevent_default::Builder::new()
|
||||
.with_flags(Flags::all().difference(Flags::CONTEXT_MENU))
|
||||
.build()
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn prevent_default() -> tauri::plugin::TauriPlugin<tauri::Wry> {
|
||||
tauri_plugin_prevent_default::Builder::new().build()
|
||||
tauri_plugin_prevent_default::Builder::new().build()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"productName": "Coop",
|
||||
"productName": "COOP",
|
||||
"version": "0.2.0",
|
||||
"identifier": "su.reya.coop",
|
||||
"build": {
|
||||
|
||||
@@ -120,9 +120,9 @@ async getCurrentAccount() : Promise<Result<string, string>> {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getMetadata(userId: string) : Promise<Result<string, string>> {
|
||||
async getMetadata(id: string) : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_metadata", { userId }) };
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_metadata", { id }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import type {
|
||||
AsyncStorage,
|
||||
MaybePromise,
|
||||
PersistedQuery,
|
||||
} from "@tanstack/query-persist-client-core";
|
||||
import { ask, message, open } from "@tauri-apps/plugin-dialog";
|
||||
import { readFile } from "@tauri-apps/plugin-fs";
|
||||
import {
|
||||
@@ -5,6 +10,7 @@ import {
|
||||
requestPermission,
|
||||
} from "@tauri-apps/plugin-notification";
|
||||
import { relaunch } from "@tauri-apps/plugin-process";
|
||||
import type { LazyStore } from "@tauri-apps/plugin-store";
|
||||
import { check } from "@tauri-apps/plugin-updater";
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import dayjs from "dayjs";
|
||||
@@ -13,22 +19,6 @@ import updateLocale from "dayjs/plugin/updateLocale";
|
||||
import { type NostrEvent, nip19 } from "nostr-tools";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(updateLocale);
|
||||
|
||||
dayjs.updateLocale("en", {
|
||||
relativeTime: {
|
||||
past: "%s",
|
||||
s: "now",
|
||||
m: "1m",
|
||||
mm: "%dm",
|
||||
h: "1h",
|
||||
hh: "%dh",
|
||||
d: "1d",
|
||||
dd: "%dd",
|
||||
},
|
||||
});
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
@@ -52,6 +42,22 @@ export function npub(pubkey: string, len: number) {
|
||||
}
|
||||
|
||||
export function ago(time: number) {
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(updateLocale);
|
||||
|
||||
dayjs.updateLocale("en", {
|
||||
relativeTime: {
|
||||
past: "%s",
|
||||
s: "now",
|
||||
m: "1m",
|
||||
mm: "%dm",
|
||||
h: "1h",
|
||||
hh: "%dh",
|
||||
d: "1d",
|
||||
dd: "%dd",
|
||||
},
|
||||
});
|
||||
|
||||
let formated: string;
|
||||
|
||||
const now = dayjs();
|
||||
@@ -68,6 +74,22 @@ export function ago(time: number) {
|
||||
}
|
||||
|
||||
export function time(time: number) {
|
||||
dayjs.extend(relativeTime);
|
||||
dayjs.extend(updateLocale);
|
||||
|
||||
dayjs.updateLocale("en", {
|
||||
relativeTime: {
|
||||
past: "%s",
|
||||
s: "now",
|
||||
m: "1m",
|
||||
mm: "%dm",
|
||||
h: "1h",
|
||||
hh: "%dh",
|
||||
d: "1d",
|
||||
dd: "%dd",
|
||||
},
|
||||
});
|
||||
|
||||
const input = new Date(time * 1000);
|
||||
const formattedTime = input.toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
@@ -187,3 +209,14 @@ export async function upload() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function newQueryStorage(
|
||||
store: LazyStore,
|
||||
): AsyncStorage<PersistedQuery> {
|
||||
return {
|
||||
getItem: async (key) => await store.get(key),
|
||||
setItem: async (key, value) => await store.set(key, value),
|
||||
removeItem: async (key) =>
|
||||
(await store.delete(key)) as unknown as MaybePromise<void>,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,27 +1,13 @@
|
||||
import { commands } from "@/commands";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { type Metadata, useProfile } from "@/hooks/useProfile";
|
||||
import { type ReactNode, createContext, useContext } from "react";
|
||||
|
||||
type Metadata = {
|
||||
name?: string;
|
||||
display_name?: string;
|
||||
about?: string;
|
||||
website?: string;
|
||||
picture?: string;
|
||||
banner?: string;
|
||||
nip05?: string;
|
||||
lud06?: string;
|
||||
lud16?: string;
|
||||
};
|
||||
|
||||
type UserContext = {
|
||||
pubkey: string;
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
profile: Metadata | undefined;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
const UserContext = createContext<UserContext>(null);
|
||||
const UserContext = createContext<UserContext | null>(null);
|
||||
|
||||
export function UserProvider({
|
||||
pubkey,
|
||||
@@ -30,37 +16,10 @@ export function UserProvider({
|
||||
pubkey: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const {
|
||||
isLoading,
|
||||
isError,
|
||||
data: profile,
|
||||
} = useQuery({
|
||||
queryKey: ["profile", pubkey],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const normalizePubkey = pubkey
|
||||
.replace("nostr:", "")
|
||||
.replace(/[^\w\s]/gi, "");
|
||||
|
||||
const res = await commands.getMetadata(normalizePubkey);
|
||||
|
||||
if (res.status === "ok") {
|
||||
return JSON.parse(res.data) as Metadata;
|
||||
} else {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
} catch (e) {
|
||||
throw new Error(String(e));
|
||||
}
|
||||
},
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
staleTime: Number.POSITIVE_INFINITY,
|
||||
});
|
||||
const { isLoading, profile } = useProfile(pubkey);
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={{ pubkey, profile, isError, isLoading }}>
|
||||
<UserContext.Provider value={{ pubkey, profile, isLoading }}>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
);
|
||||
|
||||
66
src/hooks/useProfile.ts
Normal file
66
src/hooks/useProfile.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { commands } from "@/commands";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export type Metadata = {
|
||||
name?: string;
|
||||
display_name?: string;
|
||||
about?: string;
|
||||
website?: string;
|
||||
picture?: string;
|
||||
banner?: string;
|
||||
nip05?: string;
|
||||
lud06?: string;
|
||||
lud16?: string;
|
||||
};
|
||||
|
||||
export function useProfile(pubkey: string, data?: string) {
|
||||
const hex = useMemo(() => {
|
||||
try {
|
||||
const normalized = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, "");
|
||||
const decoded = nip19.decode(normalized);
|
||||
|
||||
switch (decoded.type) {
|
||||
case "npub":
|
||||
return decoded.data;
|
||||
case "nprofile":
|
||||
return decoded.data.pubkey;
|
||||
case "naddr":
|
||||
return decoded.data.pubkey;
|
||||
default:
|
||||
return normalized;
|
||||
}
|
||||
} catch {
|
||||
return pubkey;
|
||||
}
|
||||
}, [pubkey]);
|
||||
|
||||
const { isLoading, data: profile } = useQuery({
|
||||
queryKey: ["profile", hex],
|
||||
queryFn: async () => {
|
||||
if (data?.length) {
|
||||
const metadata: Metadata = JSON.parse(data);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
const res = await commands.getMetadata(hex);
|
||||
|
||||
if (res.status === "ok") {
|
||||
const metadata: Metadata = JSON.parse(res.data);
|
||||
return metadata;
|
||||
} else {
|
||||
await getCurrentWindow().emit("request_metadata", { id: hex });
|
||||
throw new Error(res.error);
|
||||
}
|
||||
},
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnReconnect: false,
|
||||
enabled: !!hex,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
return { isLoading, profile };
|
||||
}
|
||||
55
src/main.tsx
55
src/main.tsx
@@ -1,27 +1,43 @@
|
||||
import { experimental_createPersister } from "@tanstack/query-persist-client-core";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||
import { type } from "@tauri-apps/plugin-os";
|
||||
import { LRUCache } from "lru-cache";
|
||||
import { LazyStore } from "@tauri-apps/plugin-store";
|
||||
import { type ReactNode, StrictMode } from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { newQueryStorage } from "./commons";
|
||||
|
||||
// Global CSS
|
||||
import "./global.css";
|
||||
// Import the generated commands
|
||||
import { commands } from "./commands";
|
||||
// Import the generated route tree
|
||||
import { routeTree } from "./routes.gen";
|
||||
|
||||
// Register the router instance for type safety
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
|
||||
const platform = type();
|
||||
const queryClient = new QueryClient();
|
||||
const chatManager = new LRUCache<string, string>({
|
||||
max: 3,
|
||||
dispose: async (v, _) => await commands.disconnectInboxRelays(v),
|
||||
const store = new LazyStore(".data", { autoSave: 300 });
|
||||
const storage = newQueryStorage(store);
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
gcTime: 1000 * 20, // 20 seconds
|
||||
persister: experimental_createPersister({
|
||||
storage: storage,
|
||||
maxAge: 1000 * 60 * 60 * 6, // 6 hours
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: {
|
||||
queryClient,
|
||||
chatManager,
|
||||
platform,
|
||||
},
|
||||
Wrap: ({ children }: { children: ReactNode }) => {
|
||||
@@ -31,20 +47,11 @@ const router = createRouter({
|
||||
},
|
||||
});
|
||||
|
||||
// Register the router instance for type safety
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
const rootElement = document.getElementById("root");
|
||||
const root = ReactDOM.createRoot(rootElement as unknown as HTMLElement);
|
||||
|
||||
// Render the app
|
||||
const rootElement = document.getElementById("root") as HTMLElement;
|
||||
if (!rootElement.innerHTML) {
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</StrictMode>,
|
||||
);
|
||||
}
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/* prettier-ignore-start */
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file is auto-generated by TanStack Router
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
@@ -38,26 +38,31 @@ const AccountLayoutChatsNewLazyImport = createFileRoute(
|
||||
// Create/Update Routes
|
||||
|
||||
const AccountRoute = AccountImport.update({
|
||||
id: '/$account',
|
||||
path: '/$account',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const ResetLazyRoute = ResetLazyImport.update({
|
||||
id: '/reset',
|
||||
path: '/reset',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/reset.lazy').then((d) => d.Route))
|
||||
|
||||
const NewLazyRoute = NewLazyImport.update({
|
||||
id: '/new',
|
||||
path: '/new',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route))
|
||||
|
||||
const InboxRelaysRoute = InboxRelaysImport.update({
|
||||
id: '/inbox-relays',
|
||||
path: '/inbox-relays',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/inbox-relays.lazy').then((d) => d.Route))
|
||||
|
||||
const BootstrapRelaysRoute = BootstrapRelaysImport.update({
|
||||
id: '/bootstrap-relays',
|
||||
path: '/bootstrap-relays',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() =>
|
||||
@@ -65,21 +70,25 @@ const BootstrapRelaysRoute = BootstrapRelaysImport.update({
|
||||
)
|
||||
|
||||
const IndexRoute = IndexImport.update({
|
||||
id: '/',
|
||||
path: '/',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))
|
||||
|
||||
const AuthNewRoute = AuthNewImport.update({
|
||||
id: '/auth/new',
|
||||
path: '/auth/new',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const AuthImportRoute = AuthImportImport.update({
|
||||
id: '/auth/import',
|
||||
path: '/auth/import',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const AuthConnectRoute = AuthConnectImport.update({
|
||||
id: '/auth/connect',
|
||||
path: '/auth/connect',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
@@ -90,6 +99,7 @@ const AccountLayoutRoute = AccountLayoutImport.update({
|
||||
} as any)
|
||||
|
||||
const AccountLayoutChatsLazyRoute = AccountLayoutChatsLazyImport.update({
|
||||
id: '/chats',
|
||||
path: '/chats',
|
||||
getParentRoute: () => AccountLayoutRoute,
|
||||
} as any).lazy(() =>
|
||||
@@ -97,6 +107,7 @@ const AccountLayoutChatsLazyRoute = AccountLayoutChatsLazyImport.update({
|
||||
)
|
||||
|
||||
const AccountLayoutContactsRoute = AccountLayoutContactsImport.update({
|
||||
id: '/contacts',
|
||||
path: '/contacts',
|
||||
getParentRoute: () => AccountLayoutRoute,
|
||||
} as any).lazy(() =>
|
||||
@@ -104,6 +115,7 @@ const AccountLayoutContactsRoute = AccountLayoutContactsImport.update({
|
||||
)
|
||||
|
||||
const AccountLayoutChatsNewLazyRoute = AccountLayoutChatsNewLazyImport.update({
|
||||
id: '/new',
|
||||
path: '/new',
|
||||
getParentRoute: () => AccountLayoutChatsLazyRoute,
|
||||
} as any).lazy(() =>
|
||||
@@ -111,6 +123,7 @@ const AccountLayoutChatsNewLazyRoute = AccountLayoutChatsNewLazyImport.update({
|
||||
)
|
||||
|
||||
const AccountLayoutChatsIdRoute = AccountLayoutChatsIdImport.update({
|
||||
id: '/$id',
|
||||
path: '/$id',
|
||||
getParentRoute: () => AccountLayoutChatsLazyRoute,
|
||||
} as any).lazy(() =>
|
||||
@@ -393,8 +406,6 @@ export const routeTree = rootRoute
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
|
||||
/* prettier-ignore-end */
|
||||
|
||||
/* ROUTE_MANIFEST_START
|
||||
{
|
||||
"routes": {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { message } from "@tauri-apps/plugin-dialog";
|
||||
import type { NostrEvent } from "nostr-tools";
|
||||
import {
|
||||
type Dispatch,
|
||||
type RefObject,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useRef,
|
||||
@@ -154,13 +155,12 @@ function List() {
|
||||
if (!group.includes(sender)) return;
|
||||
if (!group.some((item) => receivers.includes(item))) return;
|
||||
|
||||
await queryClient.setQueryData(
|
||||
["chats", id],
|
||||
(prevEvents: NostrEvent[]) => {
|
||||
if (!prevEvents) return [event];
|
||||
return [event, ...prevEvents];
|
||||
},
|
||||
);
|
||||
queryClient.setQueryData(["chats", id], (prevEvents: NostrEvent[]) => {
|
||||
if (!prevEvents) return [event];
|
||||
return [event, ...prevEvents];
|
||||
});
|
||||
|
||||
await queryClient.invalidateQueries({ queryKey: ["chats", id] });
|
||||
});
|
||||
|
||||
return () => {
|
||||
@@ -189,7 +189,7 @@ function List() {
|
||||
className="relative h-full py-2 [&>div]:!flex [&>div]:flex-col [&>div]:justify-end [&>div]:min-h-full"
|
||||
>
|
||||
<Virtualizer
|
||||
scrollRef={scrollRef}
|
||||
scrollRef={scrollRef as unknown as RefObject<HTMLElement>}
|
||||
ref={ref}
|
||||
shift={true}
|
||||
onScroll={(offset) => {
|
||||
@@ -218,12 +218,12 @@ function List() {
|
||||
Cannot load message. Please try again later.
|
||||
</div>
|
||||
</div>
|
||||
) : !data.length ? (
|
||||
) : !data?.length ? (
|
||||
<div className="h-20 flex items-center justify-center">
|
||||
<CoopIcon className="size-10 text-neutral-200 dark:text-neutral-800" />
|
||||
</div>
|
||||
) : (
|
||||
data.map((item) => (
|
||||
data?.map((item) => (
|
||||
<div
|
||||
key={item[0]}
|
||||
className="w-full flex flex-col items-center mt-3 gap-3"
|
||||
@@ -233,8 +233,10 @@ function List() {
|
||||
</div>
|
||||
<div className="w-full">
|
||||
{item[1]
|
||||
.sort((a, b) => a.created_at - b.created_at)
|
||||
.map((item, idx) => renderItem(item, idx))}
|
||||
? item[1]
|
||||
.sort((a, b) => a.created_at - b.created_at)
|
||||
.map((item, idx) => renderItem(item, idx))
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
|
||||
@@ -1,34 +1,31 @@
|
||||
import { commands } from '@/commands'
|
||||
import { Spinner } from '@/components/spinner'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { commands } from "@/commands";
|
||||
import { Spinner } from "@/components/spinner";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute('/$account/_layout/chats/$id')({
|
||||
loader: async ({ params, context }) => {
|
||||
const res = await commands.connectInboxRelays(params.id, false)
|
||||
export const Route = createFileRoute("/$account/_layout/chats/$id")({
|
||||
loader: async ({ params }) => {
|
||||
const res = await commands.connectInboxRelays(params.id, false);
|
||||
|
||||
if (res.status === 'ok') {
|
||||
// Add id to chat manager to unsubscribe later.
|
||||
context.chatManager.set(params.id, params.id)
|
||||
|
||||
return res.data
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
},
|
||||
pendingComponent: Pending,
|
||||
pendingMs: 200,
|
||||
pendingMinMs: 100,
|
||||
})
|
||||
if (res.status === "ok") {
|
||||
return res.data;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
pendingComponent: Pending,
|
||||
pendingMs: 200,
|
||||
pendingMinMs: 100,
|
||||
});
|
||||
|
||||
function Pending() {
|
||||
return (
|
||||
<div className="size-full flex items-center justify-center">
|
||||
<div className="flex flex-col gap-2 items-center justify-center">
|
||||
<Spinner />
|
||||
<span className="text-xs text-center text-neutral-600 dark:text-neutral-400">
|
||||
Connection in progress. Please wait ...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="size-full flex items-center justify-center">
|
||||
<div className="flex flex-col gap-2 items-center justify-center">
|
||||
<Spinner />
|
||||
<span className="text-xs text-center text-neutral-600 dark:text-neutral-400">
|
||||
Connection in progress. Please wait ...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,14 @@ import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { open } from "@tauri-apps/plugin-shell";
|
||||
import { type NostrEvent, nip19 } from "nostr-tools";
|
||||
import { useCallback, useEffect, useRef, useState, useTransition } from "react";
|
||||
import {
|
||||
type RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useTransition,
|
||||
} from "react";
|
||||
import { Virtualizer } from "virtua";
|
||||
|
||||
type EventPayload = {
|
||||
@@ -122,10 +129,12 @@ function ChatList() {
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = listen<EventPayload>("event", async (data) => {
|
||||
const event: NostrEvent = JSON.parse(data.payload.event);
|
||||
const chats: NostrEvent[] = await queryClient.getQueryData(["chats"]);
|
||||
const chats: NostrEvent[] | undefined = await queryClient.getQueryData([
|
||||
"chats",
|
||||
]);
|
||||
|
||||
if (chats) {
|
||||
const event: NostrEvent = JSON.parse(data.payload.event);
|
||||
const index = chats.findIndex((item) => item.pubkey === event.pubkey);
|
||||
|
||||
if (index === -1) {
|
||||
@@ -140,11 +149,13 @@ function ChatList() {
|
||||
);
|
||||
} else {
|
||||
const newEvents = [...chats];
|
||||
|
||||
newEvents[index] = {
|
||||
...event,
|
||||
};
|
||||
|
||||
await queryClient.setQueryData(["chats"], newEvents);
|
||||
queryClient.setQueryData(["chats"], newEvents);
|
||||
await queryClient.invalidateQueries({ queryKey: ["chats"] });
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -173,14 +184,14 @@ function ChatList() {
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : isSync && !data.length ? (
|
||||
) : isSync && !data?.length ? (
|
||||
<div className="p-2">
|
||||
<div className="px-2 h-12 w-full rounded-lg bg-black/5 dark:bg-white/5 flex items-center justify-center text-sm">
|
||||
No chats.
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
data.map((item) => (
|
||||
data?.map((item) => (
|
||||
<Link
|
||||
key={item.id + item.pubkey}
|
||||
to="/$account/chats/$id"
|
||||
@@ -410,7 +421,10 @@ function Compose() {
|
||||
ref={scrollRef}
|
||||
className="relative h-full p-2"
|
||||
>
|
||||
<Virtualizer scrollRef={scrollRef} overscan={1}>
|
||||
<Virtualizer
|
||||
scrollRef={scrollRef as unknown as RefObject<HTMLElement>}
|
||||
overscan={1}
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="h-[400px] flex items-center justify-center">
|
||||
<Spinner className="size-4" />
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { cn } from "@/commons";
|
||||
import type { Metadata } from "@/hooks/useProfile";
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import type { OsType } from "@tauri-apps/plugin-os";
|
||||
import type { LRUCache } from "lru-cache";
|
||||
import type { NostrEvent } from "nostr-tools";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface RouterContext {
|
||||
queryClient: QueryClient;
|
||||
chatManager: LRUCache<string, string, unknown>;
|
||||
platform: OsType;
|
||||
}
|
||||
|
||||
@@ -15,7 +17,29 @@ export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
});
|
||||
|
||||
function RootComponent() {
|
||||
const { platform } = Route.useRouteContext();
|
||||
const { platform, queryClient } = Route.useRouteContext();
|
||||
|
||||
useEffect(() => {
|
||||
const unlisten = getCurrentWindow().listen<string>(
|
||||
"metadata",
|
||||
async (data) => {
|
||||
const event: NostrEvent = JSON.parse(data.payload);
|
||||
const metadata: Metadata = JSON.parse(event.content);
|
||||
|
||||
// Update query cache
|
||||
queryClient.setQueryData(["profile", event.pubkey], () => metadata);
|
||||
|
||||
// Reset query cache
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["profile", event.pubkey],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
unlisten.then((f) => f());
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"strictNullChecks": false
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
||||
Reference in New Issue
Block a user