wip: refactor

This commit is contained in:
2024-11-05 08:47:24 +07:00
parent 578ed5550b
commit ebf87812e9
25 changed files with 1868 additions and 1284 deletions

View File

@@ -1,2 +0,0 @@
/src
/public

View File

@@ -15,42 +15,44 @@
"@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-progress": "^1.1.0",
"@radix-ui/react-scroll-area": "^1.2.0", "@radix-ui/react-scroll-area": "^1.2.0",
"@tanstack/react-query": "^5.59.8", "@tanstack/query-broadcast-client-experimental": "^5.59.17",
"@tanstack/react-router": "^1.64.0", "@tanstack/query-persist-client-core": "^5.59.17",
"@tauri-apps/api": "2.0.2", "@tanstack/react-query": "^5.59.19",
"@tauri-apps/plugin-clipboard-manager": "2.0.0", "@tanstack/react-router": "^1.78.3",
"@tauri-apps/plugin-dialog": "2.0.0", "@tauri-apps/api": "^2.0.3",
"@tauri-apps/plugin-fs": "2.0.0", "@tauri-apps/plugin-clipboard-manager": "^2.0.0",
"@tauri-apps/plugin-notification": "2.0.0", "@tauri-apps/plugin-dialog": "^2.0.1",
"@tauri-apps/plugin-os": "2.0.0", "@tauri-apps/plugin-fs": "^2.0.1",
"@tauri-apps/plugin-process": "2.0.0", "@tauri-apps/plugin-notification": "^2.0.0",
"@tauri-apps/plugin-shell": "2.0.0", "@tauri-apps/plugin-os": "^2.0.0",
"@tauri-apps/plugin-updater": "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", "dayjs": "^1.11.13",
"lru-cache": "^11.0.1",
"minidenticons": "^4.2.1", "minidenticons": "^4.2.1",
"nostr-tools": "^2.7.2", "nostr-tools": "^2.10.1",
"react": "19.0.0-rc-d025ddd3-20240722", "react": "19.0.0-rc-d025ddd3-20240722",
"react-dom": "19.0.0-rc-d025ddd3-20240722", "react-dom": "19.0.0-rc-d025ddd3-20240722",
"unique-names-generator": "^4.7.1", "unique-names-generator": "^4.7.1",
"virtua": "^0.35.0" "virtua": "^0.36.2"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.3", "@biomejs/biome": "1.9.3",
"@tanstack/router-plugin": "^1.64.0", "@tanstack/router-plugin": "^1.78.3",
"@tauri-apps/cli": "2.0.2", "@tauri-apps/cli": "2.0.2",
"@types/react": "npm:types-react@19.0.0-rc.1", "@types/react": "npm:types-react@19.0.0-rc.1",
"@types/react-dom": "npm:types-react-dom@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", "autoprefixer": "^10.4.20",
"babel-plugin-react-compiler": "0.0.0-experimental-696af53-20240625", "babel-plugin-react-compiler": "0.0.0-experimental-696af53-20240625",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"postcss": "^8.4.47", "postcss": "^8.4.47",
"tailwind-gradient-mask-image": "^1.2.0", "tailwind-gradient-mask-image": "^1.2.0",
"tailwind-merge": "^2.5.3", "tailwind-merge": "^2.5.4",
"tailwindcss": "^3.4.13", "tailwindcss": "^3.4.14",
"typescript": "^5.6.3", "typescript": "^5.6.3",
"vite": "^5.4.8", "vite": "^5.4.10",
"vite-tsconfig-paths": "^5.0.1" "vite-tsconfig-paths": "^5.0.1"
}, },
"overrides": { "overrides": {

876
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

472
src-tauri/Cargo.lock generated
View File

@@ -2,6 +2,39 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 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]] [[package]]
name = "Inflector" name = "Inflector"
version = "0.11.4" version = "0.11.4"
@@ -109,9 +142,9 @@ checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13"
[[package]] [[package]]
name = "arbitrary" name = "arbitrary"
version = "1.3.2" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" checksum = "775a8770d29db3dadcb858482cc240af7b2ffde4ac4de67d1d4955728103f0e2"
dependencies = [ dependencies = [
"derive_arbitrary", "derive_arbitrary",
] ]
@@ -619,7 +652,7 @@ dependencies = [
[[package]] [[package]]
name = "border" name = "border"
version = "0.1.0" 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 = [ dependencies = [
"cocoa 0.25.0", "cocoa 0.25.0",
"color", "color",
@@ -935,7 +968,7 @@ dependencies = [
[[package]] [[package]]
name = "color" name = "color"
version = "0.1.0" 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 = [ dependencies = [
"cocoa 0.25.0", "cocoa 0.25.0",
"objc", "objc",
@@ -973,36 +1006,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" 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]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@@ -1256,9 +1259,9 @@ dependencies = [
[[package]] [[package]]
name = "derive_arbitrary" name = "derive_arbitrary"
version = "1.3.2" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" checksum = "d475dfebcb4854d596b17b09f477616f80f17a550517f2b3615d8c205d5c802b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2193,9 +2196,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.0" version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
dependencies = [ dependencies = [
"allocator-api2", "allocator-api2",
"equivalent", "equivalent",
@@ -2454,6 +2457,124 @@ dependencies = [
"png", "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]] [[package]]
name = "ident_case" name = "ident_case"
version = "1.0.1" version = "1.0.1"
@@ -2462,12 +2583,23 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.5.0" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
dependencies = [ dependencies = [
"unicode-bidi", "idna_adapter",
"unicode-normalization", "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]] [[package]]
@@ -2527,7 +2659,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.15.0", "hashbrown 0.15.1",
"serde", "serde",
] ]
@@ -2760,7 +2892,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f8fe839464d4e4b37d756d7e910063696af79a7e877282cb1825e4ec5f10833" checksum = "2f8fe839464d4e4b37d756d7e910063696af79a7e877282cb1825e4ec5f10833"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"linux-keyutils",
"log", "log",
"security-framework 2.11.1", "security-framework 2.11.1",
"security-framework 3.0.0", "security-framework 3.0.0",
@@ -2770,8 +2901,7 @@ dependencies = [
[[package]] [[package]]
name = "keyring-search" name = "keyring-search"
version = "1.2.1" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/reyamir/keyring-search#59d54e6a28229f09f87b9b043690ee8a1d63221e"
checksum = "8fba83ff0a0efb658afeaaa6de89c7abd3ccd34333f5a36d5dae417334fcd488"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"lazy_static", "lazy_static",
@@ -2865,7 +2995,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets 0.48.5", "windows-targets 0.52.6",
] ]
[[package]] [[package]]
@@ -2918,6 +3048,12 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "litemap"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
[[package]] [[package]]
name = "lmdb-master-sys" name = "lmdb-master-sys"
version = "0.2.4" version = "0.2.4"
@@ -2972,7 +3108,7 @@ version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
dependencies = [ dependencies = [
"hashbrown 0.15.0", "hashbrown 0.15.1",
] ]
[[package]] [[package]]
@@ -3098,9 +3234,9 @@ dependencies = [
[[package]] [[package]]
name = "muda" name = "muda"
version = "0.15.2" version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b18047edf23933de40835403d4b9211ffd1dcc65c0eec569df38a1fb8aebd719" checksum = "fdae9c00e61cc0579bcac625e8ad22104c60548a025bfc972dc83868a28e1484"
dependencies = [ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"dpi", "dpi",
@@ -3213,7 +3349,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]] [[package]]
name = "nostr" name = "nostr"
version = "0.35.0" version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b" source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
dependencies = [ dependencies = [
"aes", "aes",
"async-trait", "async-trait",
@@ -3244,7 +3380,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-connect" name = "nostr-connect"
version = "0.35.0" version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b" source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"async-utility", "async-utility",
@@ -3258,7 +3394,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-database" name = "nostr-database"
version = "0.35.0" version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b" source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"flatbuffers", "flatbuffers",
@@ -3272,7 +3408,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-lmdb" name = "nostr-lmdb"
version = "0.35.0" version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b" source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
dependencies = [ dependencies = [
"heed", "heed",
"nostr", "nostr",
@@ -3285,7 +3421,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-relay-pool" name = "nostr-relay-pool"
version = "0.35.0" version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b" source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"async-wsocket", "async-wsocket",
@@ -3303,7 +3439,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-sdk" name = "nostr-sdk"
version = "0.35.0" version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b" source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"atomic-destructor", "atomic-destructor",
@@ -3322,7 +3458,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-zapper" name = "nostr-zapper"
version = "0.35.0" version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b" source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"nostr", "nostr",
@@ -3342,6 +3478,16 @@ dependencies = [
"zbus", "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]] [[package]]
name = "num" name = "num"
version = "0.4.3" version = "0.4.3"
@@ -3456,7 +3602,7 @@ dependencies = [
[[package]] [[package]]
name = "nwc" name = "nwc"
version = "0.35.0" version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b" source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"async-utility", "async-utility",
@@ -3794,6 +3940,12 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "page_size" name = "page_size"
version = "0.6.0" version = "0.6.0"
@@ -4638,9 +4790,9 @@ dependencies = [
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.38" version = "0.38.39"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"errno", "errno",
@@ -5046,6 +5198,15 @@ dependencies = [
"digest", "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]] [[package]]
name = "shared_child" name = "shared_child"
version = "1.0.1" version = "1.0.1"
@@ -5338,6 +5499,17 @@ dependencies = [
"crossbeam-queue", "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]] [[package]]
name = "sys-locale" name = "sys-locale"
version = "0.3.2" version = "0.3.2"
@@ -5714,6 +5886,22 @@ dependencies = [
"tokio", "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]] [[package]]
name = "tauri-plugin-updater" name = "tauri-plugin-updater"
version = "2.0.2" version = "2.0.2"
@@ -5906,24 +6094,34 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.67" version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3c6efbfc763e64eb85c11c25320f0737cb7364c4b6336db90aa9ebe27a0bbd" checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.67" version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b607164372e89797d78b8e23a6d67d5d1038c1c65efd52e1389ef8b77caba2a6" checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.87", "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]] [[package]]
name = "tiff" name = "tiff"
version = "0.9.1" version = "0.9.1"
@@ -5966,6 +6164,16 @@ dependencies = [
"time-core", "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]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.8.0" version = "1.8.0"
@@ -5991,6 +6199,7 @@ dependencies = [
"bytes", "bytes",
"libc", "libc",
"mio", "mio",
"parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2", "socket2",
@@ -6167,6 +6376,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [ dependencies = [
"once_cell", "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]] [[package]]
@@ -6280,12 +6515,6 @@ dependencies = [
"unic-common", "unic-common",
] ]
[[package]]
name = "unicode-bidi"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893"
[[package]] [[package]]
name = "unicode-ident" name = "unicode-ident"
version = "1.0.13" version = "1.0.13"
@@ -6325,9 +6554,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.2" version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna",
@@ -6353,6 +6582,18 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 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]] [[package]]
name = "uuid" name = "uuid"
version = "1.11.0" version = "1.11.0"
@@ -6374,6 +6615,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]] [[package]]
name = "version-compare" name = "version-compare"
version = "0.2.0" version = "0.2.0"
@@ -6704,7 +6951,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -7125,6 +7372,18 @@ dependencies = [
"windows-sys 0.48.0", "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]] [[package]]
name = "wry" name = "wry"
version = "0.46.3" version = "0.46.3"
@@ -7225,6 +7484,30 @@ dependencies = [
"windows-sys 0.59.0", "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]] [[package]]
name = "zbus" name = "zbus"
version = "4.0.1" version = "4.0.1"
@@ -7311,12 +7594,55 @@ dependencies = [
"syn 2.0.87", "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]] [[package]]
name = "zeroize" name = "zeroize"
version = "1.8.1" version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 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]] [[package]]
name = "zip" name = "zip"
version = "2.2.0" version = "2.2.0"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "coop" name = "COOP"
version = "0.1.0" version = "0.2.0"
description = "direct message client for desktop" description = "direct message client for desktop"
authors = ["npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445"] authors = ["npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445"]
repository = "https://github.com/lumehq/coop" repository = "https://github.com/lumehq/coop"
@@ -24,6 +24,7 @@ tauri-plugin-updater = "2.0.0"
tauri-plugin-process = "2.0.0" tauri-plugin-process = "2.0.0"
tauri-plugin-fs = "2.0.0" tauri-plugin-fs = "2.0.0"
tauri-plugin-notification = "2.0.0" tauri-plugin-notification = "2.0.0"
tauri-plugin-store = "2.1.0"
tauri-plugin-decorum = "1.1.0" tauri-plugin-decorum = "1.1.0"
tauri-plugin-prevent-default = "^0.4" tauri-plugin-prevent-default = "^0.4"
tauri-specta = { version = "2.0.0-rc", features = ["derive", "typescript"] } 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 = "^2.0.0-rc.20"
specta-typescript = "0.0.7" specta-typescript = "0.0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
itertools = "0.13.0" itertools = "0.13.0"
futures = "0.3.30" futures = "0.3.30"
keyring-search = "1.2.0" keyring = { version = "3", features = ["apple-native", "windows-native"] }
keyring = { version = "3", features = [ keyring-search = { git = "https://github.com/reyamir/keyring-search" }
"apple-native", tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
"windows-native",
"linux-native",
] }
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
border = { git = "https://github.com/ahkohd/tauri-toolkit", branch = "v2" } border = { git = "https://github.com/ahkohd/tauri-toolkit", branch = "v2" }

View File

@@ -14,11 +14,6 @@
"core:resources:default", "core:resources:default",
"core:menu:default", "core:menu:default",
"core:tray: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-close",
"core:window:allow-center", "core:window:allow-center",
"core:window:allow-minimize", "core:window:allow-minimize",
@@ -27,14 +22,18 @@
"core:window:allow-set-focus", "core:window:allow-set-focus",
"core:window:allow-start-dragging", "core:window:allow-start-dragging",
"core:window:allow-toggle-maximize", "core:window:allow-toggle-maximize",
"shell:allow-open",
"dialog:default",
"dialog:allow-open",
"dialog:allow-ask",
"dialog:allow-message",
"decorum:allow-show-snap-overlay", "decorum:allow-show-snap-overlay",
"prevent-default:default", "prevent-default:default",
"updater:default", "updater:default",
"updater:allow-check",
"updater:allow-download-and-install",
"clipboard-manager:allow-write-text", "clipboard-manager:allow-write-text",
"clipboard-manager:allow-read-text", "clipboard-manager:allow-read-text",
"fs:allow-read-file", "fs:allow-read-file",
"notification:default" "notification:default",
"store:default"
] ]
} }

View File

@@ -1,4 +1,5 @@
wss://purplepag.es/, wss://relay.damus.io,
wss://directory.yabu.me/, wss://relay.primal.net,
wss://user.kindpag.es/, wss://nostr.fmt.wiz.biz,
wss://relay.damus.io/, wss://directory.yabu.me,
wss://purplepag.es,

View File

@@ -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/"]

View File

@@ -5,429 +5,371 @@ use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specta::Type; use specta::Type;
use std::{collections::HashSet, str::FromStr, time::Duration}; use std::{collections::HashSet, str::FromStr, time::Duration};
use tauri::{Emitter, Manager, State}; use tauri::{Manager, State};
use tauri_plugin_notification::NotificationExt;
use crate::Nostr; use crate::{Nostr, SUBSCRIPTION_ID};
#[derive(Clone, Serialize)] #[derive(Clone, Serialize)]
pub struct EventPayload { pub struct EventPayload {
event: String, // JSON String event: String, // JSON String
sender: String, sender: String,
} }
#[derive(Debug, Clone, Serialize, Deserialize, Type)] #[derive(Debug, Clone, Serialize, Deserialize, Type)]
struct Account { struct Account {
password: String, password: String,
nostr_connect: Option<String>, nostr_connect: Option<String>,
} }
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn get_metadata(id: String, state: State<'_, Nostr>) -> Result<String, String> { pub async fn get_metadata(id: String, state: State<'_, Nostr>) -> Result<String, String> {
let client = &state.client; let client = &state.client;
let public_key = PublicKey::parse(&id).map_err(|e| e.to_string())?; 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() { match events.first() {
Some(event) => match Metadata::from_json(&event.content) { Some(event) => match Metadata::from_json(&event.content) {
Ok(metadata) => Ok(metadata.as_json()), Ok(metadata) => Ok(metadata.as_json()),
Err(e) => Err(e.to_string()), Err(e) => Err(e.to_string()),
}, },
None => Err("Metadata not found".into()), None => Err("Metadata not found".into()),
} }
} }
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub fn get_accounts() -> Vec<String> { pub fn get_accounts() -> Vec<String> {
let search = Search::new().expect("Unexpected."); let search = Search::new().expect("Unexpected.");
let results = search.by_service("Coop Secret Storage"); let results = search.by_service("Coop Secret Storage");
let list = List::list_credentials(&results, Limit::All); let list = List::list_credentials(&results, Limit::All);
let accounts: HashSet<String> = let accounts: HashSet<String> = list
list.split_whitespace().filter(|v| v.starts_with("npub1")).map(String::from).collect(); .split_whitespace()
.filter(|v| v.starts_with("npub1"))
.map(String::from)
.collect();
accounts.into_iter().collect() accounts.into_iter().collect()
} }
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn get_current_account(state: State<'_, Nostr>) -> Result<String, String> { pub async fn get_current_account(state: State<'_, Nostr>) -> Result<String, String> {
let client = &state.client; let client = &state.client;
let signer = client.signer().await.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 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 bech32 = public_key.to_bech32().map_err(|e| e.to_string())?;
Ok(bech32) Ok(bech32)
} }
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn create_account( pub async fn create_account(
name: String, name: String,
about: String, about: String,
picture: String, picture: String,
password: String, password: String,
state: State<'_, Nostr>, state: State<'_, Nostr>,
) -> Result<String, String> { ) -> Result<String, String> {
let client = &state.client; let client = &state.client;
let keys = Keys::generate(); let keys = Keys::generate();
let npub = keys.public_key().to_bech32().map_err(|e| e.to_string())?; let npub = keys.public_key().to_bech32().map_err(|e| e.to_string())?;
let secret_key = keys.secret_key(); let secret_key = keys.secret_key();
let enc = EncryptedSecretKey::new(secret_key, password, 16, KeySecurity::Medium) let enc = EncryptedSecretKey::new(secret_key, password, 16, KeySecurity::Medium)
.map_err(|err| err.to_string())?; .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())?;
// Save account // Save account
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 {
let j = serde_json::to_string(&account).map_err(|e| e.to_string())?; password: enc_bech32,
let _ = keyring.set_password(&j); nostr_connect: None,
};
let j = serde_json::to_string(&account).map_err(|e| e.to_string())?;
let _ = keyring.set_password(&j);
// Update signer // Update signer
client.set_signer(keys).await; client.set_signer(keys).await;
let mut metadata = let mut metadata = Metadata::new()
Metadata::new().display_name(name.clone()).name(name.to_lowercase()).about(about); .display_name(name.clone())
.name(name.to_lowercase())
.about(about);
if let Ok(url) = Url::parse(&picture) { if let Ok(url) = Url::parse(&picture) {
metadata = metadata.picture(url) metadata = metadata.picture(url)
} }
match client.set_metadata(&metadata).await { match client.set_metadata(&metadata).await {
Ok(_) => Ok(npub), Ok(_) => Ok(npub),
Err(e) => Err(e.to_string()), Err(e) => Err(e.to_string()),
} }
} }
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn import_account(key: String, password: String) -> Result<String, String> { pub async fn import_account(key: String, password: String) -> Result<String, String> {
let (npub, enc_bech32) = match key.starts_with("ncryptsec") { let (npub, enc_bech32) = match key.starts_with("ncryptsec") {
true => { true => {
let enc = EncryptedSecretKey::from_bech32(key).map_err(|err| err.to_string())?; 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 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 secret_key = enc.to_secret_key(password).map_err(|err| err.to_string())?;
let keys = Keys::new(secret_key); let keys = Keys::new(secret_key);
let npub = keys.public_key().to_bech32().unwrap(); let npub = keys.public_key().to_bech32().unwrap();
(npub, enc_bech32) (npub, enc_bech32)
} }
false => { false => {
let secret_key = SecretKey::from_bech32(key).map_err(|err| err.to_string())?; let secret_key = SecretKey::from_bech32(key).map_err(|err| err.to_string())?;
let keys = Keys::new(secret_key.clone()); let keys = Keys::new(secret_key.clone());
let npub = keys.public_key().to_bech32().unwrap(); let npub = keys.public_key().to_bech32().unwrap();
let enc = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Medium) let enc = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Medium)
.map_err(|err| err.to_string())?; .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())?; let pwd = serde_json::to_string(&account).map_err(|e| e.to_string())?;
keyring.set_password(&pwd).map_err(|e| e.to_string())?; keyring.set_password(&pwd).map_err(|e| e.to_string())?;
Ok(npub) Ok(npub)
} }
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn connect_account(uri: String, state: State<'_, Nostr>) -> Result<String, String> { 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()) { match NostrConnectURI::parse(uri.clone()) {
Ok(bunker_uri) => { Ok(bunker_uri) => {
// Local user // Local user
let app_keys = Keys::generate(); let app_keys = Keys::generate();
let app_secret = app_keys.secret_key().to_secret_hex(); let app_secret = app_keys.secret_key().to_secret_hex();
// Get remote user // Get remote user
let remote_user = bunker_uri.remote_signer_public_key().unwrap(); let remote_user = bunker_uri.remote_signer_public_key().unwrap();
let remote_npub = remote_user.to_bech32().unwrap(); let remote_npub = remote_user.to_bech32().unwrap();
match NostrConnect::new(bunker_uri, app_keys, Duration::from_secs(120), None) { match NostrConnect::new(bunker_uri, app_keys, Duration::from_secs(120), None) {
Ok(signer) => { Ok(signer) => {
let mut url = Url::parse(&uri).unwrap(); let mut url = Url::parse(&uri).unwrap();
let query: Vec<(String, String)> = url let query: Vec<(String, String)> = url
.query_pairs() .query_pairs()
.filter(|(name, _)| name != "secret") .filter(|(name, _)| name != "secret")
.map(|(name, value)| (name.into_owned(), value.into_owned())) .map(|(name, value)| (name.into_owned(), value.into_owned()))
.collect(); .collect();
url.query_pairs_mut().clear().extend_pairs(&query); url.query_pairs_mut().clear().extend_pairs(&query);
let key = format!("{}_nostrconnect", remote_npub); let key = format!("{}_nostrconnect", remote_npub);
let keyring = Entry::new("Coop Secret Storage", &key).unwrap(); let keyring = Entry::new("Coop Secret Storage", &key).unwrap();
let account = let account = Account {
Account { password: app_secret, nostr_connect: Some(url.to_string()) }; password: app_secret,
let j = serde_json::to_string(&account).map_err(|e| e.to_string())?; nostr_connect: Some(url.to_string()),
let _ = keyring.set_password(&j); };
let j = serde_json::to_string(&account).map_err(|e| e.to_string())?;
let _ = keyring.set_password(&j);
// Update signer // Update signer
let _ = client.set_signer(signer).await; let _ = client.set_signer(signer).await;
Ok(remote_npub) Ok(remote_npub)
} }
Err(err) => Err(err.to_string()), Err(err) => Err(err.to_string()),
} }
} }
Err(err) => Err(err.to_string()), Err(err) => Err(err.to_string()),
} }
} }
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn reset_password(key: String, password: String) -> Result<(), String> { 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 secret_key = SecretKey::from_bech32(key).map_err(|err| err.to_string())?;
let keys = Keys::new(secret_key.clone()); let keys = Keys::new(secret_key.clone());
let npub = keys.public_key().to_bech32().unwrap(); let npub = keys.public_key().to_bech32().unwrap();
let enc = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Medium) let enc = EncryptedSecretKey::new(&secret_key, password, 16, KeySecurity::Medium)
.map_err(|err| err.to_string())?; .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())?;
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 {
let j = serde_json::to_string(&account).map_err(|e| e.to_string())?; password: enc_bech32,
let _ = keyring.set_password(&j); 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] #[tauri::command]
#[specta::specta] #[specta::specta]
pub fn delete_account(id: String) -> Result<(), String> { pub fn delete_account(id: String) -> Result<(), String> {
let keyring = Entry::new("Coop Secret Storage", &id).map_err(|e| e.to_string())?; let keyring = Entry::new("Coop Secret Storage", &id).map_err(|e| e.to_string())?;
let _ = keyring.delete_credential(); let _ = keyring.delete_credential();
Ok(()) Ok(())
} }
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn get_contact_list(state: State<'_, Nostr>) -> Result<Vec<String>, String> { 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 { match client.get_contact_list(Some(Duration::from_secs(10))).await {
Ok(contacts) => { Ok(contacts) => {
let list = contacts.into_iter().map(|c| c.public_key.to_hex()).collect::<Vec<_>>(); let list = contacts
Ok(list) .into_iter()
} .map(|c| c.public_key.to_hex())
Err(e) => Err(e.to_string()), .collect::<Vec<_>>();
} Ok(list)
}
Err(e) => Err(e.to_string()),
}
} }
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn login( pub async fn login(
account: String, account: String,
password: String, password: String,
state: State<'_, Nostr>, state: State<'_, Nostr>,
handle: tauri::AppHandle, handle: tauri::AppHandle,
) -> Result<String, String> { ) -> Result<String, String> {
let client = &state.client; let client = &state.client;
let keyring = Entry::new("Coop Secret Storage", &account).map_err(|e| e.to_string())?; let keyring = Entry::new("Coop Secret Storage", &account).map_err(|e| e.to_string())?;
let account = match keyring.get_password() { let account = match keyring.get_password() {
Ok(pw) => { Ok(pw) => {
let account: Account = serde_json::from_str(&pw).map_err(|e| e.to_string())?; let account: Account = serde_json::from_str(&pw).map_err(|e| e.to_string())?;
account account
} }
Err(e) => return Err(e.to_string()), Err(e) => return Err(e.to_string()),
}; };
let public_key = match account.nostr_connect { let public_key = match account.nostr_connect {
None => { None => {
let ncryptsec = let ncryptsec =
EncryptedSecretKey::from_bech32(account.password).map_err(|e| e.to_string())?; EncryptedSecretKey::from_bech32(account.password).map_err(|e| e.to_string())?;
let secret_key = ncryptsec.to_secret_key(password).map_err(|_| "Wrong password.")?; let secret_key = ncryptsec
let keys = Keys::new(secret_key); .to_secret_key(password)
let public_key = keys.public_key(); .map_err(|_| "Wrong password.")?;
let keys = Keys::new(secret_key);
let public_key = keys.public_key();
// Update signer // Update signer
client.set_signer(keys).await; client.set_signer(keys).await;
public_key public_key
} }
Some(bunker) => { Some(bunker) => {
let uri = NostrConnectURI::parse(bunker).map_err(|e| e.to_string())?; let uri = NostrConnectURI::parse(bunker).map_err(|e| e.to_string())?;
let public_key = uri.remote_signer_public_key().unwrap().clone(); 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())?; 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) { match NostrConnect::new(uri, app_keys, Duration::from_secs(120), None) {
Ok(signer) => { Ok(signer) => {
// Update signer // Update signer
client.set_signer(signer).await; client.set_signer(signer).await;
// Return public key // Return public key
public_key public_key
} }
Err(e) => return Err(e.to_string()), 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) = let mut rx = client
client.get_events_of(vec![inbox], EventSource::relays(Some(Duration::from_secs(3)))).await .stream_events(vec![filter], Some(Duration::from_secs(3)))
{ .await
if let Some(event) = events.into_iter().next() { .map_err(|e| e.to_string())?;
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<_>>();
for url in urls.iter() { while let Some(event) = rx.next().await {
if let Err(e) = client.add_relay(url).await { let urls = event
println!("Connect relay failed: {}", e) .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 for url in urls.iter() {
// TODO: remove this let _ = client.add_relay(url).await;
let _ = client let _ = client.connect_relay(url).await;
.get_events_from( }
urls.clone(),
vec![Filter::new().kind(Kind::TextNote).limit(0)],
Some(Duration::from_secs(5)),
)
.await;
let mut inbox_relays = state.inbox_relays.lock().await; let mut inbox_relays = state.inbox_relays.write().await;
inbox_relays.insert(public_key, urls); inbox_relays.insert(public_key, urls);
} else { }
return Err("404".into());
}
}
let sub_id = SubscriptionId::new("inbox"); tauri::async_runtime::spawn(async move {
let new_message = Filter::new().kind(Kind::GiftWrap).pubkey(public_key).limit(0); let state = handle.state::<Nostr>();
let client = &state.client;
if client.subscription(&sub_id).await.is_some() { let inbox_relays = state.inbox_relays.read().await;
// Remove old subscriotion let relays = inbox_relays.get(&public_key).unwrap().to_owned();
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;
}
tauri::async_runtime::spawn(async move { let sub_id = SubscriptionId::new(SUBSCRIPTION_ID);
let state = handle.state::<Nostr>(); let new_message = Filter::new()
let client = &state.client; .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. let filter = Filter::new()
// TODO: Find better way to save unsigned event to database. .kind(Kind::GiftWrap)
let fake_sig = Signature::from_str("f9e79d141c004977192d05a86f81ec7c585179c371f7350a5412d33575a2a356433f58e405c2296ed273e2fe0aafa25b641e39cc4e1f3f261ebf55bce0cbac83").unwrap(); .pubkey(public_key)
.limit(200);
if let Ok(events) = client let mut rx = client
.pool() .stream_events_from(&relays, vec![filter], Some(Duration::from_secs(40)))
.get_events_of( .await
vec![filter], .unwrap();
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,
);
if let Err(e) = client.database().save_event(&ev).await { while let Some(event) = rx.next().await {
println!("Error: {}", e) println!("Event: {}", event.as_json());
} }
}
}
handle.emit("synchronized", ()).unwrap();
}
client // handle.emit("synchronized", ()).unwrap();
.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,
);
// Save rumor to database to further query Ok(public_key.to_hex())
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())
} }

View File

@@ -8,89 +8,110 @@ use crate::Nostr;
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn get_chats(state: State<'_, Nostr>) -> Result<Vec<String>, String> { pub async fn get_chats(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
let client = &state.client; 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 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 { let filter = Filter::new()
Ok(events) => { .kind(Kind::PrivateDirectMessage)
let ev = events .pubkey(public_key);
.into_iter() let events = client
.sorted_by_key(|ev| Reverse(ev.created_at)) .database()
.filter(|ev| ev.pubkey != public_key) .query(vec![filter])
.unique_by(|ev| ev.pubkey) .await
.map(|ev| ev.as_json()) .map_err(|e| e.to_string())?;
.collect::<Vec<_>>();
Ok(ev) let data = events
} .into_iter()
Err(e) => Err(e.to_string()), .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] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn get_chat_messages(id: String, state: State<'_, Nostr>) -> Result<Vec<String>, String> { pub async fn get_chat_messages(id: String, state: State<'_, Nostr>) -> Result<Vec<String>, String> {
let client = &state.client; let client = &state.client;
let signer = client.signer().await.map_err(|e| e.to_string())?; 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 receiver = signer.get_public_key().await.map_err(|e| e.to_string())?;
let sender = PublicKey::parse(id).map_err(|e| e.to_string())?; let sender = PublicKey::parse(id).map_err(|e| e.to_string())?;
let recv_filter = let recv_filter = Filter::new()
Filter::new().kind(Kind::PrivateDirectMessage).author(sender).pubkey(receiver); .kind(Kind::PrivateDirectMessage)
let sender_filter = .author(sender)
Filter::new().kind(Kind::PrivateDirectMessage).author(receiver).pubkey(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 { match client
Ok(events) => { .database()
let ev = events.into_iter().map(|ev| ev.as_json()).collect::<Vec<_>>(); .query(vec![recv_filter, sender_filter])
Ok(ev) .await
} {
Err(e) => Err(e.to_string()), Ok(events) => {
} let ev = events
.into_iter()
.map(|ev| ev.as_json())
.collect::<Vec<_>>();
Ok(ev)
}
Err(e) => Err(e.to_string()),
}
} }
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn send_message( pub async fn send_message(
to: String, to: String,
message: String, message: String,
state: State<'_, Nostr>, state: State<'_, Nostr>,
) -> Result<(), String> { ) -> Result<(), String> {
let client = &state.client; let client = &state.client;
let relays = state.inbox_relays.lock().await; let relays = state.inbox_relays.read().await;
let signer = client.signer().await.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 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 receiver = PublicKey::parse(&to).map_err(|e| e.to_string())?;
// TODO: Add support reply_to // TODO: Add support reply_to
let rumor = EventBuilder::private_msg_rumor(receiver, message, None); let rumor = EventBuilder::private_msg_rumor(receiver, message, None);
// Get inbox relays per member // Get inbox relays for receiver
let outbox_urls = match relays.get(&receiver) { let outbox_urls = match relays.get(&receiver) {
Some(relays) => relays, Some(relays) => relays,
None => return Err("Receiver didn't have inbox relays to receive message.".into()), None => return Err("Receiver didn't have inbox relays to receive message.".into()),
}; };
let inbox_urls = match relays.get(&public_key) { // Get inbox relays for current user
Some(relays) => relays, let inbox_urls = match relays.get(&public_key) {
None => return Err("Please config inbox relays to backup your message.".into()), Some(relays) => relays,
}; None => return Err("Please config inbox relays to backup your message.".into()),
};
// Send message to [receiver] // Send message to [receiver]
match client.gift_wrap_to(outbox_urls, &receiver, rumor.clone(), None).await { match client
Ok(_) => { .gift_wrap_to(outbox_urls, &receiver, rumor.clone(), None)
// Send message to [yourself] .await
if let Err(e) = client.gift_wrap_to(inbox_urls, &public_key, rumor, None).await { {
return Err(e.to_string()); 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(()) Ok(())
} }
Err(e) => Err(e.to_string()), Err(e) => Err(e.to_string()),
} }
} }

View File

@@ -1,8 +1,8 @@
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use std::{ use std::{
fs::OpenOptions, fs::OpenOptions,
io::{self, BufRead, Write}, io::{self, BufRead, Write},
time::Duration, time::Duration,
}; };
use tauri::{Manager, State}; use tauri::{Manager, State};
@@ -11,177 +11,198 @@ use crate::Nostr;
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub fn get_bootstrap_relays(app: tauri::AppHandle) -> Result<Vec<String>, String> { pub fn get_bootstrap_relays(app: tauri::AppHandle) -> Result<Vec<String>, String> {
let relays_path = app let relays_path = app
.path() .path()
.resolve("resources/relays.txt", tauri::path::BaseDirectory::Resource) .resolve("resources/relays.txt", tauri::path::BaseDirectory::Resource)
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let file = std::fs::File::open(relays_path).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 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] #[tauri::command]
#[specta::specta] #[specta::specta]
pub fn set_bootstrap_relays(relays: String, app: tauri::AppHandle) -> Result<(), String> { pub fn set_bootstrap_relays(relays: String, app: tauri::AppHandle) -> Result<(), String> {
let relays_path = app let relays_path = app
.path() .path()
.resolve("resources/relays.txt", tauri::path::BaseDirectory::Resource) .resolve("resources/relays.txt", tauri::path::BaseDirectory::Resource)
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let mut file = OpenOptions::new().write(true).open(relays_path).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] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn get_inbox_relays( pub async fn get_inbox_relays(
user_id: String, user_id: String,
state: State<'_, Nostr>, state: State<'_, Nostr>,
) -> Result<Vec<String>, String> { ) -> Result<Vec<String>, String> {
let client = &state.client; let client = &state.client;
let public_key = PublicKey::parse(user_id).map_err(|e| e.to_string())?; 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 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 let mut events = Events::new(&[filter.clone()]);
{
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<_>>();
Ok(urls) let mut rx = client
} else { .stream_events(vec![filter], Some(Duration::from_secs(3)))
Ok(Vec::new()) .await
} .map_err(|e| e.to_string())?;
}
Err(e) => Err(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] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn ensure_inbox_relays( pub async fn ensure_inbox_relays(
user_id: String, user_id: String,
state: State<'_, Nostr>, state: State<'_, Nostr>,
) -> Result<Vec<String>, String> { ) -> Result<Vec<String>, String> {
let public_key = PublicKey::parse(user_id).map_err(|e| e.to_string())?; let public_key = PublicKey::parse(user_id).map_err(|e| e.to_string())?;
let relays = state.inbox_relays.lock().await; let relays = state.inbox_relays.read().await;
match relays.get(&public_key) { match relays.get(&public_key) {
Some(relays) => { Some(relays) => {
if relays.is_empty() { if relays.is_empty() {
Err("404".into()) Err("404".into())
} else { } else {
Ok(relays.to_owned()) Ok(relays.to_owned())
} }
} }
None => Err("404".into()), None => Err("404".into()),
} }
} }
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn set_inbox_relays(relays: Vec<String>, state: State<'_, Nostr>) -> Result<(), String> { 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 tags = relays
let event = EventBuilder::new(Kind::Custom(10050), "", tags); .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 { match client.send_event_builder(event).await {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(e) => Err(e.to_string()), Err(e) => Err(e.to_string()),
} }
} }
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn connect_inbox_relays( pub async fn connect_inbox_relays(
user_id: String, user_id: String,
ignore_cache: bool, ignore_cache: bool,
state: State<'_, Nostr>, state: State<'_, Nostr>,
) -> Result<Vec<String>, String> { ) -> Result<Vec<String>, String> {
let client = &state.client; let client = &state.client;
let public_key = PublicKey::parse(&user_id).map_err(|e| e.to_string())?; 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.write().await;
let mut inbox_relays = state.inbox_relays.lock().await;
if !ignore_cache { if !ignore_cache {
if let Some(relays) = inbox_relays.get(&public_key) { if let Some(relays) = inbox_relays.get(&public_key) {
for url in relays { for url in relays {
if let Ok(relay) = client.relay(url).await { if let Ok(relay) = client.relay(url).await {
if !relay.is_connected() { if !relay.is_connected() {
if let Err(e) = client.connect_relay(url).await { if let Err(e) = client.connect_relay(url).await {
println!("Connect relay failed: {}", e) println!("Connect relay failed: {}", e)
} }
} }
} else if let Err(e) = client.add_relay(url).await { } else if let Err(e) = client.add_relay(url).await {
println!("Connect relay failed: {}", e) println!("Connect relay failed: {}", e)
} }
} }
return Ok(relays.to_owned()); 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 let mut events = Events::new(&[filter.clone()]);
{
Ok(events) => {
let mut relays = Vec::new();
if let Some(event) = events.into_iter().next() { let mut rx = client
for tag in event.tags.iter() { .stream_events(vec![filter], Some(Duration::from_secs(3)))
if let Some(TagStandard::Relay(relay)) = tag.as_standardized() { .await
let url = relay.to_string(); .map_err(|e| e.to_string())?;
if let Err(e) = client.add_relay(&url).await { while let Some(event) = rx.next().await {
println!("Connect relay failed: {}", e) events.insert(event);
}; }
relays.push(url) if let Some(event) = events.first() {
} let mut relays = Vec::new();
}
// Update state for tag in event.tags.iter() {
inbox_relays.insert(public_key, relays.clone()); 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 relays.push(url)
// disconnect_nip65_relays(nip65_relays, client).await; }
} }
Ok(relays) // Update state
} inbox_relays.insert(public_key, relays.clone());
Err(e) => Err(e.to_string()),
} Ok(relays)
} else {
Err("User's inbox relays not found.".to_string())
}
} }
#[tauri::command] #[tauri::command]
#[specta::specta] #[specta::specta]
pub async fn disconnect_inbox_relays( pub async fn disconnect_inbox_relays(
user_id: String, user_id: String,
state: State<'_, Nostr>, state: State<'_, Nostr>,
) -> Result<(), String> { ) -> Result<(), String> {
let client = &state.client; let client = &state.client;
let public_key = PublicKey::parse(&user_id).map_err(|e| e.to_string())?; let public_key = PublicKey::parse(&user_id).map_err(|e| e.to_string())?;
let inbox_relays = state.inbox_relays.lock().await; let inbox_relays = state.inbox_relays.read().await;
if let Some(relays) = inbox_relays.get(&public_key) { if let Some(relays) = inbox_relays.get(&public_key) {
for relay in relays { for relay in relays {
let _ = client.disconnect_relay(relay).await; let _ = client.disconnect_relay(relay).await;
} }
} }
Ok(()) Ok(())
} }

View File

@@ -5,60 +5,52 @@ use tauri::{Manager, WebviewWindowBuilder};
use tauri_plugin_decorum::WebviewWindowExt; use tauri_plugin_decorum::WebviewWindowExt;
pub fn setup_tray_icon(app: &tauri::AppHandle) -> tauri::Result<()> { 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) let menu = tauri::menu::MenuBuilder::new(app)
.item( .item(
&tauri::menu::MenuItem::with_id(app, "open", "Open Coop", true, None::<&str>).unwrap(), &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()) .item(&tauri::menu::MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).unwrap())
.build() .build()
.expect("Error: cannot create menu."); .expect("Error: cannot create menu.");
if tray.set_menu(Some(menu)).is_err() { if tray.set_menu(Some(menu)).is_err() {
panic!("Error: cannot set menu for tray icon.") panic!("Error: cannot set menu for tray icon.")
} }
tray.on_menu_event(move |app, event| match event.id.0.as_str() { tray.on_menu_event(move |app, event| match event.id.0.as_str() {
"open" => { "open" => {
if let Some(window) = app.get_webview_window("main") { if let Some(window) = app.get_webview_window("main") {
if window.is_visible().unwrap_or_default() { if window.is_visible().unwrap_or_default() {
let _ = window.set_focus(); let _ = window.set_focus();
} else { } else {
let _ = window.show(); let _ = window.show();
let _ = window.set_focus(); let _ = window.set_focus();
}; };
} else { } else {
let config = app.config().app.windows.first().unwrap(); let config = app.config().app.windows.first().unwrap();
let window = let window = WebviewWindowBuilder::from_config(app, config)
WebviewWindowBuilder::from_config(app, config).unwrap().build().unwrap(); .unwrap()
.build()
.unwrap();
// Set custom decoration // Set custom decoration
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
window.create_overlay_titlebar().unwrap(); window.create_overlay_titlebar().unwrap();
// Set traffic light inset // Set traffic light inset
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
window.set_traffic_lights_inset(12.0, 18.0).unwrap(); window.set_traffic_lights_inset(12.0, 18.0).unwrap();
// Workaround for reset traffic light when theme changed // Restore native border
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let win_ = window.clone(); window.add_border(None);
#[cfg(target_os = "macos")] }
window.on_window_event(move |event| { }
if let tauri::WindowEvent::ThemeChanged(_) = event { "quit" => std::process::exit(0),
win_.set_traffic_lights_inset(12.0, 18.0).unwrap(); _ => {}
} });
});
// Restore native border Ok(())
#[cfg(target_os = "macos")]
window.add_border(None);
}
}
"quit" => std::process::exit(0),
_ => {}
});
Ok(())
} }

View File

@@ -3,65 +3,91 @@
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
use border::WebviewWindowExt as WebviewWindowExtAlt; use border::WebviewWindowExt as WebviewWindowExtAlt;
use commands::tray::setup_tray_icon;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
use specta_typescript::Typescript; use specta_typescript::Typescript;
use std::{ use std::{
collections::HashMap, collections::{HashMap, HashSet},
env, fs, env, fs,
io::{self, BufRead}, io::{self, BufRead},
str::FromStr, str::FromStr,
}; };
use tauri::{async_runtime::Mutex, Manager}; use tauri::{Emitter, Listener, Manager};
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
use tauri_plugin_decorum::WebviewWindowExt; use tauri_plugin_decorum::WebviewWindowExt;
use tauri_specta::{collect_commands, Builder}; 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; mod commands;
pub struct Nostr { pub struct Nostr {
client: Client, client: Client,
inbox_relays: Mutex<HashMap<PublicKey, Vec<String>>>, 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() { fn main() {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1"); std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
let builder = Builder::<tauri::Wry>::new().commands(collect_commands![ let builder = Builder::<tauri::Wry>::new().commands(collect_commands![
get_bootstrap_relays, get_bootstrap_relays,
set_bootstrap_relays, set_bootstrap_relays,
get_inbox_relays, get_inbox_relays,
set_inbox_relays, set_inbox_relays,
ensure_inbox_relays, ensure_inbox_relays,
connect_inbox_relays, connect_inbox_relays,
disconnect_inbox_relays, disconnect_inbox_relays,
login, login,
create_account, create_account,
import_account, import_account,
connect_account, connect_account,
delete_account, delete_account,
reset_password, reset_password,
get_accounts, get_accounts,
get_current_account, get_current_account,
get_metadata, get_metadata,
get_contact_list, get_contact_list,
get_chats, get_chats,
get_chat_messages, get_chat_messages,
send_message, send_message,
]); ]);
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
builder builder
.export(Typescript::default(), "../src/commands.ts") .export(Typescript::default(), "../src/commands.ts")
.expect("Failed to export typescript bindings"); .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()) .invoke_handler(builder.invoke_handler())
.setup(move |app| { .setup(move |app| {
let handle = app.handle(); let handle = app.handle();
let handle_clone = handle.clone();
let handle_clone_child = handle_clone.clone();
// Setup tray icon
let _ = setup_tray_icon(handle); let _ = setup_tray_icon(handle);
#[cfg(not(target_os = "linux"))] #[cfg(not(target_os = "linux"))]
@@ -88,11 +114,10 @@ fn main() {
let _ = fs::create_dir_all(&dir); let _ = fs::create_dir_all(&dir);
// Setup database // Setup database
let database = NostrLMDB::open(dir.join("nostr-lmdb")) let database = NostrLMDB::open(dir.join("nostr-lmdb")).expect("Error: cannot create database.");
.expect("Error: cannot create database.");
// Setup nostr client // 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(); let client = ClientBuilder::default().opts(opts).database(database).build();
// Add bootstrap relays // Add bootstrap relays
@@ -123,15 +148,128 @@ fn main() {
} }
} }
let _ = client.add_discovery_relay("wss://user.kindpag.es/").await;
// Connect
client.connect().await;
client client
}); });
// Create global state // 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(()) Ok(())
}) })
.plugin(prevent_default()) .plugin(prevent_default())
.plugin(tauri_plugin_store::Builder::default().build())
.plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_updater::Builder::new().build())
@@ -152,14 +290,14 @@ fn main() {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
fn prevent_default() -> tauri::plugin::TauriPlugin<tauri::Wry> { 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() tauri_plugin_prevent_default::Builder::new()
.with_flags(Flags::all().difference(Flags::CONTEXT_MENU)) .with_flags(Flags::all().difference(Flags::CONTEXT_MENU))
.build() .build()
} }
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
fn prevent_default() -> tauri::plugin::TauriPlugin<tauri::Wry> { fn prevent_default() -> tauri::plugin::TauriPlugin<tauri::Wry> {
tauri_plugin_prevent_default::Builder::new().build() tauri_plugin_prevent_default::Builder::new().build()
} }

View File

@@ -1,5 +1,5 @@
{ {
"productName": "Coop", "productName": "COOP",
"version": "0.2.0", "version": "0.2.0",
"identifier": "su.reya.coop", "identifier": "su.reya.coop",
"build": { "build": {

View File

@@ -120,9 +120,9 @@ async getCurrentAccount() : Promise<Result<string, string>> {
else return { status: "error", error: e as any }; 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 { try {
return { status: "ok", data: await TAURI_INVOKE("get_metadata", { userId }) }; return { status: "ok", data: await TAURI_INVOKE("get_metadata", { id }) };
} catch (e) { } catch (e) {
if(e instanceof Error) throw e; if(e instanceof Error) throw e;
else return { status: "error", error: e as any }; else return { status: "error", error: e as any };

View File

@@ -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 { ask, message, open } from "@tauri-apps/plugin-dialog";
import { readFile } from "@tauri-apps/plugin-fs"; import { readFile } from "@tauri-apps/plugin-fs";
import { import {
@@ -5,6 +10,7 @@ import {
requestPermission, requestPermission,
} from "@tauri-apps/plugin-notification"; } from "@tauri-apps/plugin-notification";
import { relaunch } from "@tauri-apps/plugin-process"; import { relaunch } from "@tauri-apps/plugin-process";
import type { LazyStore } from "@tauri-apps/plugin-store";
import { check } from "@tauri-apps/plugin-updater"; import { check } from "@tauri-apps/plugin-updater";
import { type ClassValue, clsx } from "clsx"; import { type ClassValue, clsx } from "clsx";
import dayjs from "dayjs"; import dayjs from "dayjs";
@@ -13,22 +19,6 @@ import updateLocale from "dayjs/plugin/updateLocale";
import { type NostrEvent, nip19 } from "nostr-tools"; import { type NostrEvent, nip19 } from "nostr-tools";
import { twMerge } from "tailwind-merge"; 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[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
} }
@@ -52,6 +42,22 @@ export function npub(pubkey: string, len: number) {
} }
export function ago(time: 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; let formated: string;
const now = dayjs(); const now = dayjs();
@@ -68,6 +74,22 @@ export function ago(time: number) {
} }
export function time(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 input = new Date(time * 1000);
const formattedTime = input.toLocaleTimeString([], { const formattedTime = input.toLocaleTimeString([], {
hour: "2-digit", hour: "2-digit",
@@ -187,3 +209,14 @@ export async function upload() {
return null; 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>,
};
}

View File

@@ -1,27 +1,13 @@
import { commands } from "@/commands"; import { type Metadata, useProfile } from "@/hooks/useProfile";
import { useQuery } from "@tanstack/react-query";
import { type ReactNode, createContext, useContext } from "react"; 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 = { type UserContext = {
pubkey: string; pubkey: string;
isLoading: boolean;
isError: boolean;
profile: Metadata | undefined; profile: Metadata | undefined;
isLoading: boolean;
}; };
const UserContext = createContext<UserContext>(null); const UserContext = createContext<UserContext | null>(null);
export function UserProvider({ export function UserProvider({
pubkey, pubkey,
@@ -30,37 +16,10 @@ export function UserProvider({
pubkey: string; pubkey: string;
children: ReactNode; children: ReactNode;
}) { }) {
const { const { isLoading, profile } = useProfile(pubkey);
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,
});
return ( return (
<UserContext.Provider value={{ pubkey, profile, isError, isLoading }}> <UserContext.Provider value={{ pubkey, profile, isLoading }}>
{children} {children}
</UserContext.Provider> </UserContext.Provider>
); );

66
src/hooks/useProfile.ts Normal file
View 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 };
}

View File

@@ -1,27 +1,43 @@
import { experimental_createPersister } from "@tanstack/query-persist-client-core";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { RouterProvider, createRouter } from "@tanstack/react-router"; import { RouterProvider, createRouter } from "@tanstack/react-router";
import { type } from "@tauri-apps/plugin-os"; 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 { type ReactNode, StrictMode } from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import { newQueryStorage } from "./commons";
// Global CSS
import "./global.css"; import "./global.css";
// Import the generated commands
import { commands } from "./commands";
// Import the generated route tree // Import the generated route tree
import { routeTree } from "./routes.gen"; 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 platform = type();
const queryClient = new QueryClient(); const store = new LazyStore(".data", { autoSave: 300 });
const chatManager = new LRUCache<string, string>({ const storage = newQueryStorage(store);
max: 3, const queryClient = new QueryClient({
dispose: async (v, _) => await commands.disconnectInboxRelays(v), defaultOptions: {
queries: {
gcTime: 1000 * 20, // 20 seconds
persister: experimental_createPersister({
storage: storage,
maxAge: 1000 * 60 * 60 * 6, // 6 hours
}),
},
},
}); });
const router = createRouter({ const router = createRouter({
routeTree, routeTree,
context: { context: {
queryClient, queryClient,
chatManager,
platform, platform,
}, },
Wrap: ({ children }: { children: ReactNode }) => { Wrap: ({ children }: { children: ReactNode }) => {
@@ -31,20 +47,11 @@ const router = createRouter({
}, },
}); });
// Register the router instance for type safety const rootElement = document.getElementById("root");
declare module "@tanstack/react-router" { const root = ReactDOM.createRoot(rootElement as unknown as HTMLElement);
interface Register {
router: typeof router;
}
}
// Render the app root.render(
const rootElement = document.getElementById("root") as HTMLElement; <StrictMode>
if (!rootElement.innerHTML) { <RouterProvider router={router} />
const root = ReactDOM.createRoot(rootElement); </StrictMode>,
root.render( );
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
);
}

View File

@@ -1,12 +1,12 @@
/* prettier-ignore-start */
/* eslint-disable */ /* eslint-disable */
// @ts-nocheck // @ts-nocheck
// noinspection JSUnusedGlobalSymbols // 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' import { createFileRoute } from '@tanstack/react-router'
@@ -38,26 +38,31 @@ const AccountLayoutChatsNewLazyImport = createFileRoute(
// Create/Update Routes // Create/Update Routes
const AccountRoute = AccountImport.update({ const AccountRoute = AccountImport.update({
id: '/$account',
path: '/$account', path: '/$account',
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const ResetLazyRoute = ResetLazyImport.update({ const ResetLazyRoute = ResetLazyImport.update({
id: '/reset',
path: '/reset', path: '/reset',
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/reset.lazy').then((d) => d.Route)) } as any).lazy(() => import('./routes/reset.lazy').then((d) => d.Route))
const NewLazyRoute = NewLazyImport.update({ const NewLazyRoute = NewLazyImport.update({
id: '/new',
path: '/new', path: '/new',
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route)) } as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route))
const InboxRelaysRoute = InboxRelaysImport.update({ const InboxRelaysRoute = InboxRelaysImport.update({
id: '/inbox-relays',
path: '/inbox-relays', path: '/inbox-relays',
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/inbox-relays.lazy').then((d) => d.Route)) } as any).lazy(() => import('./routes/inbox-relays.lazy').then((d) => d.Route))
const BootstrapRelaysRoute = BootstrapRelaysImport.update({ const BootstrapRelaysRoute = BootstrapRelaysImport.update({
id: '/bootstrap-relays',
path: '/bootstrap-relays', path: '/bootstrap-relays',
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any).lazy(() => } as any).lazy(() =>
@@ -65,21 +70,25 @@ const BootstrapRelaysRoute = BootstrapRelaysImport.update({
) )
const IndexRoute = IndexImport.update({ const IndexRoute = IndexImport.update({
id: '/',
path: '/', path: '/',
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route)) } as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))
const AuthNewRoute = AuthNewImport.update({ const AuthNewRoute = AuthNewImport.update({
id: '/auth/new',
path: '/auth/new', path: '/auth/new',
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const AuthImportRoute = AuthImportImport.update({ const AuthImportRoute = AuthImportImport.update({
id: '/auth/import',
path: '/auth/import', path: '/auth/import',
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const AuthConnectRoute = AuthConnectImport.update({ const AuthConnectRoute = AuthConnectImport.update({
id: '/auth/connect',
path: '/auth/connect', path: '/auth/connect',
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
@@ -90,6 +99,7 @@ const AccountLayoutRoute = AccountLayoutImport.update({
} as any) } as any)
const AccountLayoutChatsLazyRoute = AccountLayoutChatsLazyImport.update({ const AccountLayoutChatsLazyRoute = AccountLayoutChatsLazyImport.update({
id: '/chats',
path: '/chats', path: '/chats',
getParentRoute: () => AccountLayoutRoute, getParentRoute: () => AccountLayoutRoute,
} as any).lazy(() => } as any).lazy(() =>
@@ -97,6 +107,7 @@ const AccountLayoutChatsLazyRoute = AccountLayoutChatsLazyImport.update({
) )
const AccountLayoutContactsRoute = AccountLayoutContactsImport.update({ const AccountLayoutContactsRoute = AccountLayoutContactsImport.update({
id: '/contacts',
path: '/contacts', path: '/contacts',
getParentRoute: () => AccountLayoutRoute, getParentRoute: () => AccountLayoutRoute,
} as any).lazy(() => } as any).lazy(() =>
@@ -104,6 +115,7 @@ const AccountLayoutContactsRoute = AccountLayoutContactsImport.update({
) )
const AccountLayoutChatsNewLazyRoute = AccountLayoutChatsNewLazyImport.update({ const AccountLayoutChatsNewLazyRoute = AccountLayoutChatsNewLazyImport.update({
id: '/new',
path: '/new', path: '/new',
getParentRoute: () => AccountLayoutChatsLazyRoute, getParentRoute: () => AccountLayoutChatsLazyRoute,
} as any).lazy(() => } as any).lazy(() =>
@@ -111,6 +123,7 @@ const AccountLayoutChatsNewLazyRoute = AccountLayoutChatsNewLazyImport.update({
) )
const AccountLayoutChatsIdRoute = AccountLayoutChatsIdImport.update({ const AccountLayoutChatsIdRoute = AccountLayoutChatsIdImport.update({
id: '/$id',
path: '/$id', path: '/$id',
getParentRoute: () => AccountLayoutChatsLazyRoute, getParentRoute: () => AccountLayoutChatsLazyRoute,
} as any).lazy(() => } as any).lazy(() =>
@@ -393,8 +406,6 @@ export const routeTree = rootRoute
._addFileChildren(rootRouteChildren) ._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>() ._addFileTypes<FileRouteTypes>()
/* prettier-ignore-end */
/* ROUTE_MANIFEST_START /* ROUTE_MANIFEST_START
{ {
"routes": { "routes": {

View File

@@ -12,6 +12,7 @@ import { message } from "@tauri-apps/plugin-dialog";
import type { NostrEvent } from "nostr-tools"; import type { NostrEvent } from "nostr-tools";
import { import {
type Dispatch, type Dispatch,
type RefObject,
type SetStateAction, type SetStateAction,
useCallback, useCallback,
useRef, useRef,
@@ -154,13 +155,12 @@ function List() {
if (!group.includes(sender)) return; if (!group.includes(sender)) return;
if (!group.some((item) => receivers.includes(item))) return; if (!group.some((item) => receivers.includes(item))) return;
await queryClient.setQueryData( queryClient.setQueryData(["chats", id], (prevEvents: NostrEvent[]) => {
["chats", id], if (!prevEvents) return [event];
(prevEvents: NostrEvent[]) => { return [event, ...prevEvents];
if (!prevEvents) return [event]; });
return [event, ...prevEvents];
}, await queryClient.invalidateQueries({ queryKey: ["chats", id] });
);
}); });
return () => { 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" className="relative h-full py-2 [&>div]:!flex [&>div]:flex-col [&>div]:justify-end [&>div]:min-h-full"
> >
<Virtualizer <Virtualizer
scrollRef={scrollRef} scrollRef={scrollRef as unknown as RefObject<HTMLElement>}
ref={ref} ref={ref}
shift={true} shift={true}
onScroll={(offset) => { onScroll={(offset) => {
@@ -218,12 +218,12 @@ function List() {
Cannot load message. Please try again later. Cannot load message. Please try again later.
</div> </div>
</div> </div>
) : !data.length ? ( ) : !data?.length ? (
<div className="h-20 flex items-center justify-center"> <div className="h-20 flex items-center justify-center">
<CoopIcon className="size-10 text-neutral-200 dark:text-neutral-800" /> <CoopIcon className="size-10 text-neutral-200 dark:text-neutral-800" />
</div> </div>
) : ( ) : (
data.map((item) => ( data?.map((item) => (
<div <div
key={item[0]} key={item[0]}
className="w-full flex flex-col items-center mt-3 gap-3" className="w-full flex flex-col items-center mt-3 gap-3"
@@ -233,8 +233,10 @@ function List() {
</div> </div>
<div className="w-full"> <div className="w-full">
{item[1] {item[1]
.sort((a, b) => a.created_at - b.created_at) ? item[1]
.map((item, idx) => renderItem(item, idx))} .sort((a, b) => a.created_at - b.created_at)
.map((item, idx) => renderItem(item, idx))
: null}
</div> </div>
</div> </div>
)) ))

View File

@@ -1,34 +1,31 @@
import { commands } from '@/commands' import { commands } from "@/commands";
import { Spinner } from '@/components/spinner' import { Spinner } from "@/components/spinner";
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute('/$account/_layout/chats/$id')({ export const Route = createFileRoute("/$account/_layout/chats/$id")({
loader: async ({ params, context }) => { loader: async ({ params }) => {
const res = await commands.connectInboxRelays(params.id, false) const res = await commands.connectInboxRelays(params.id, false);
if (res.status === 'ok') { if (res.status === "ok") {
// Add id to chat manager to unsubscribe later. return res.data;
context.chatManager.set(params.id, params.id) } else {
return [];
return res.data }
} else { },
return [] pendingComponent: Pending,
} pendingMs: 200,
}, pendingMinMs: 100,
pendingComponent: Pending, });
pendingMs: 200,
pendingMinMs: 100,
})
function Pending() { function Pending() {
return ( return (
<div className="size-full flex items-center justify-center"> <div className="size-full flex items-center justify-center">
<div className="flex flex-col gap-2 items-center justify-center"> <div className="flex flex-col gap-2 items-center justify-center">
<Spinner /> <Spinner />
<span className="text-xs text-center text-neutral-600 dark:text-neutral-400"> <span className="text-xs text-center text-neutral-600 dark:text-neutral-400">
Connection in progress. Please wait ... Connection in progress. Please wait ...
</span> </span>
</div> </div>
</div> </div>
) );
} }

View File

@@ -20,7 +20,14 @@ import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
import { message } from "@tauri-apps/plugin-dialog"; import { message } from "@tauri-apps/plugin-dialog";
import { open } from "@tauri-apps/plugin-shell"; import { open } from "@tauri-apps/plugin-shell";
import { type NostrEvent, nip19 } from "nostr-tools"; 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"; import { Virtualizer } from "virtua";
type EventPayload = { type EventPayload = {
@@ -122,10 +129,12 @@ function ChatList() {
useEffect(() => { useEffect(() => {
const unlisten = listen<EventPayload>("event", async (data) => { const unlisten = listen<EventPayload>("event", async (data) => {
const event: NostrEvent = JSON.parse(data.payload.event); const chats: NostrEvent[] | undefined = await queryClient.getQueryData([
const chats: NostrEvent[] = await queryClient.getQueryData(["chats"]); "chats",
]);
if (chats) { if (chats) {
const event: NostrEvent = JSON.parse(data.payload.event);
const index = chats.findIndex((item) => item.pubkey === event.pubkey); const index = chats.findIndex((item) => item.pubkey === event.pubkey);
if (index === -1) { if (index === -1) {
@@ -140,11 +149,13 @@ function ChatList() {
); );
} else { } else {
const newEvents = [...chats]; const newEvents = [...chats];
newEvents[index] = { newEvents[index] = {
...event, ...event,
}; };
await queryClient.setQueryData(["chats"], newEvents); queryClient.setQueryData(["chats"], newEvents);
await queryClient.invalidateQueries({ queryKey: ["chats"] });
} }
} }
}); });
@@ -173,14 +184,14 @@ function ChatList() {
</div> </div>
))} ))}
</> </>
) : isSync && !data.length ? ( ) : isSync && !data?.length ? (
<div className="p-2"> <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"> <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. No chats.
</div> </div>
</div> </div>
) : ( ) : (
data.map((item) => ( data?.map((item) => (
<Link <Link
key={item.id + item.pubkey} key={item.id + item.pubkey}
to="/$account/chats/$id" to="/$account/chats/$id"
@@ -410,7 +421,10 @@ function Compose() {
ref={scrollRef} ref={scrollRef}
className="relative h-full p-2" className="relative h-full p-2"
> >
<Virtualizer scrollRef={scrollRef} overscan={1}> <Virtualizer
scrollRef={scrollRef as unknown as RefObject<HTMLElement>}
overscan={1}
>
{isLoading ? ( {isLoading ? (
<div className="h-[400px] flex items-center justify-center"> <div className="h-[400px] flex items-center justify-center">
<Spinner className="size-4" /> <Spinner className="size-4" />

View File

@@ -1,12 +1,14 @@
import { cn } from "@/commons"; import { cn } from "@/commons";
import type { Metadata } from "@/hooks/useProfile";
import type { QueryClient } from "@tanstack/react-query"; import type { QueryClient } from "@tanstack/react-query";
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router"; import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
import { getCurrentWindow } from "@tauri-apps/api/window";
import type { OsType } from "@tauri-apps/plugin-os"; 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 { interface RouterContext {
queryClient: QueryClient; queryClient: QueryClient;
chatManager: LRUCache<string, string, unknown>;
platform: OsType; platform: OsType;
} }
@@ -15,7 +17,29 @@ export const Route = createRootRouteWithContext<RouterContext>()({
}); });
function RootComponent() { 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 ( return (
<div <div

View File

@@ -26,7 +26,6 @@
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"strictNullChecks": false
}, },
"include": [ "include": [
"src" "src"