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

876
pnpm-lock.yaml generated

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.
version = 3
[[package]]
name = "COOP"
version = "0.2.0"
dependencies = [
"border",
"futures",
"itertools 0.13.0",
"keyring",
"keyring-search",
"nostr-connect",
"nostr-sdk",
"serde",
"serde_json",
"specta",
"specta-typescript",
"tauri",
"tauri-build",
"tauri-plugin-clipboard-manager",
"tauri-plugin-decorum",
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-notification",
"tauri-plugin-os",
"tauri-plugin-prevent-default",
"tauri-plugin-process",
"tauri-plugin-shell",
"tauri-plugin-store",
"tauri-plugin-updater",
"tauri-specta",
"tokio",
"tracing-subscriber",
]
[[package]]
name = "Inflector"
version = "0.11.4"
@@ -109,9 +142,9 @@ checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13"
[[package]]
name = "arbitrary"
version = "1.3.2"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
checksum = "775a8770d29db3dadcb858482cc240af7b2ffde4ac4de67d1d4955728103f0e2"
dependencies = [
"derive_arbitrary",
]
@@ -619,7 +652,7 @@ dependencies = [
[[package]]
name = "border"
version = "0.1.0"
source = "git+https://github.com/ahkohd/tauri-toolkit?branch=v2#39952d8694268694836b5d9e5fa8473474fe4c45"
source = "git+https://github.com/ahkohd/tauri-toolkit?branch=v2#7f5223abbb672664c5ce8b45f55e71f472d53c17"
dependencies = [
"cocoa 0.25.0",
"color",
@@ -935,7 +968,7 @@ dependencies = [
[[package]]
name = "color"
version = "0.1.0"
source = "git+https://github.com/ahkohd/tauri-toolkit?branch=v2#39952d8694268694836b5d9e5fa8473474fe4c45"
source = "git+https://github.com/ahkohd/tauri-toolkit?branch=v2#7f5223abbb672664c5ce8b45f55e71f472d53c17"
dependencies = [
"cocoa 0.25.0",
"objc",
@@ -973,36 +1006,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "coop"
version = "0.1.0"
dependencies = [
"border",
"futures",
"itertools 0.13.0",
"keyring",
"keyring-search",
"nostr-connect",
"nostr-sdk",
"serde",
"serde_json",
"specta",
"specta-typescript",
"tauri",
"tauri-build",
"tauri-plugin-clipboard-manager",
"tauri-plugin-decorum",
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-notification",
"tauri-plugin-os",
"tauri-plugin-prevent-default",
"tauri-plugin-process",
"tauri-plugin-shell",
"tauri-plugin-updater",
"tauri-specta",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -1256,9 +1259,9 @@ dependencies = [
[[package]]
name = "derive_arbitrary"
version = "1.3.2"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
checksum = "d475dfebcb4854d596b17b09f477616f80f17a550517f2b3615d8c205d5c802b"
dependencies = [
"proc-macro2",
"quote",
@@ -2193,9 +2196,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.15.0"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
dependencies = [
"allocator-api2",
"equivalent",
@@ -2454,6 +2457,124 @@ dependencies = [
"png",
]
[[package]]
name = "icu_collections"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locid"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_locid_transform"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
dependencies = [
"displaydoc",
"icu_locid",
"icu_locid_transform_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_locid_transform_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
[[package]]
name = "icu_normalizer"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
dependencies = [
"displaydoc",
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"utf16_iter",
"utf8_iter",
"write16",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
[[package]]
name = "icu_properties"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locid_transform",
"icu_properties_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
[[package]]
name = "icu_provider"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
dependencies = [
"displaydoc",
"icu_locid",
"icu_provider_macros",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_provider_macros"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "ident_case"
version = "1.0.1"
@@ -2462,12 +2583,23 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.5.0"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
dependencies = [
"unicode-bidi",
"unicode-normalization",
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
@@ -2527,7 +2659,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
dependencies = [
"equivalent",
"hashbrown 0.15.0",
"hashbrown 0.15.1",
"serde",
]
@@ -2760,7 +2892,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f8fe839464d4e4b37d756d7e910063696af79a7e877282cb1825e4ec5f10833"
dependencies = [
"byteorder",
"linux-keyutils",
"log",
"security-framework 2.11.1",
"security-framework 3.0.0",
@@ -2770,8 +2901,7 @@ dependencies = [
[[package]]
name = "keyring-search"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fba83ff0a0efb658afeaaa6de89c7abd3ccd34333f5a36d5dae417334fcd488"
source = "git+https://github.com/reyamir/keyring-search#59d54e6a28229f09f87b9b043690ee8a1d63221e"
dependencies = [
"byteorder",
"lazy_static",
@@ -2865,7 +2995,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [
"cfg-if",
"windows-targets 0.48.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -2918,6 +3048,12 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "litemap"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
[[package]]
name = "lmdb-master-sys"
version = "0.2.4"
@@ -2972,7 +3108,7 @@ version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
dependencies = [
"hashbrown 0.15.0",
"hashbrown 0.15.1",
]
[[package]]
@@ -3098,9 +3234,9 @@ dependencies = [
[[package]]
name = "muda"
version = "0.15.2"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b18047edf23933de40835403d4b9211ffd1dcc65c0eec569df38a1fb8aebd719"
checksum = "fdae9c00e61cc0579bcac625e8ad22104c60548a025bfc972dc83868a28e1484"
dependencies = [
"crossbeam-channel",
"dpi",
@@ -3213,7 +3349,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]]
name = "nostr"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b"
source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
dependencies = [
"aes",
"async-trait",
@@ -3244,7 +3380,7 @@ dependencies = [
[[package]]
name = "nostr-connect"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b"
source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
dependencies = [
"async-trait",
"async-utility",
@@ -3258,7 +3394,7 @@ dependencies = [
[[package]]
name = "nostr-database"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b"
source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
dependencies = [
"async-trait",
"flatbuffers",
@@ -3272,7 +3408,7 @@ dependencies = [
[[package]]
name = "nostr-lmdb"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b"
source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
dependencies = [
"heed",
"nostr",
@@ -3285,7 +3421,7 @@ dependencies = [
[[package]]
name = "nostr-relay-pool"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b"
source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
dependencies = [
"async-utility",
"async-wsocket",
@@ -3303,7 +3439,7 @@ dependencies = [
[[package]]
name = "nostr-sdk"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b"
source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
dependencies = [
"async-utility",
"atomic-destructor",
@@ -3322,7 +3458,7 @@ dependencies = [
[[package]]
name = "nostr-zapper"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b"
source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
dependencies = [
"async-trait",
"nostr",
@@ -3342,6 +3478,16 @@ dependencies = [
"zbus",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "num"
version = "0.4.3"
@@ -3456,7 +3602,7 @@ dependencies = [
[[package]]
name = "nwc"
version = "0.35.0"
source = "git+https://github.com/rust-nostr/nostr#4da48df74e494f8705e4887ce31a63adeba7b47b"
source = "git+https://github.com/rust-nostr/nostr#b233dcf1a4769eca97a01a34e2b0f83e3eb96d3c"
dependencies = [
"async-trait",
"async-utility",
@@ -3794,6 +3940,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "page_size"
version = "0.6.0"
@@ -4638,9 +4790,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.38"
version = "0.38.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a"
checksum = "375116bee2be9ed569afe2154ea6a99dfdffd257f533f187498c2a8f5feaf4ee"
dependencies = [
"bitflags 2.6.0",
"errno",
@@ -5046,6 +5198,15 @@ dependencies = [
"digest",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shared_child"
version = "1.0.1"
@@ -5338,6 +5499,17 @@ dependencies = [
"crossbeam-queue",
]
[[package]]
name = "synstructure"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "sys-locale"
version = "0.3.2"
@@ -5714,6 +5886,22 @@ dependencies = [
"tokio",
]
[[package]]
name = "tauri-plugin-store"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9a580be53f04bb62422d239aa798e88522877f58a0d4a0e745f030055a51bb4"
dependencies = [
"dunce",
"log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror",
"tokio",
]
[[package]]
name = "tauri-plugin-updater"
version = "2.0.2"
@@ -5906,24 +6094,34 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
[[package]]
name = "thiserror"
version = "1.0.67"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3c6efbfc763e64eb85c11c25320f0737cb7364c4b6336db90aa9ebe27a0bbd"
checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.67"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b607164372e89797d78b8e23a6d67d5d1038c1c65efd52e1389ef8b77caba2a6"
checksum = "a7c61ec9a6f64d2793d8a45faba21efbe3ced62a886d44c36a009b2b519b4c7e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "thread_local"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
name = "tiff"
version = "0.9.1"
@@ -5966,6 +6164,16 @@ dependencies = [
"time-core",
]
[[package]]
name = "tinystr"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "tinyvec"
version = "1.8.0"
@@ -5991,6 +6199,7 @@ dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
@@ -6167,6 +6376,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
dependencies = [
"nu-ansi-term",
"sharded-slab",
"smallvec",
"thread_local",
"tracing-core",
"tracing-log",
]
[[package]]
@@ -6280,12 +6515,6 @@ dependencies = [
"unic-common",
]
[[package]]
name = "unicode-bidi"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893"
[[package]]
name = "unicode-ident"
version = "1.0.13"
@@ -6325,9 +6554,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "url"
version = "2.5.2"
version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada"
dependencies = [
"form_urlencoded",
"idna",
@@ -6353,6 +6582,18 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf16_iter"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.11.0"
@@ -6374,6 +6615,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "version-compare"
version = "0.2.0"
@@ -6704,7 +6951,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -7125,6 +7372,18 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "write16"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
[[package]]
name = "writeable"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "wry"
version = "0.46.3"
@@ -7225,6 +7484,30 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "yoke"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
"synstructure",
]
[[package]]
name = "zbus"
version = "4.0.1"
@@ -7311,12 +7594,55 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "zerofrom"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
[[package]]
name = "zerovec"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "zip"
version = "2.2.0"

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

@@ -3,65 +3,91 @@
#[cfg(target_os = "macos")]
use border::WebviewWindowExt as WebviewWindowExtAlt;
use commands::tray::setup_tray_icon;
use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize};
use specta_typescript::Typescript;
use std::{
collections::HashMap,
env, fs,
io::{self, BufRead},
str::FromStr,
collections::{HashMap, HashSet},
env, fs,
io::{self, BufRead},
str::FromStr,
};
use tauri::{async_runtime::Mutex, Manager};
use tauri::{Emitter, Listener, Manager};
#[cfg(not(target_os = "linux"))]
use tauri_plugin_decorum::WebviewWindowExt;
use tauri_specta::{collect_commands, Builder};
use tokio::{sync::RwLock, time::sleep, time::Duration};
use commands::{account::*, chat::*, relay::*};
use commands::{account::*, chat::*, relay::*, tray::*};
mod commands;
pub struct Nostr {
client: Client,
inbox_relays: Mutex<HashMap<PublicKey, Vec<String>>>,
client: Client,
queue: RwLock<HashSet<PublicKey>>,
inbox_relays: RwLock<HashMap<PublicKey, Vec<String>>>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Payload {
id: String,
}
#[derive(Clone, Serialize)]
pub struct EventPayload {
event: String, // JSON String
sender: String,
}
pub const QUEUE_DELAY: u64 = 300;
pub const SUBSCRIPTION_ID: &str = "inbox";
fn main() {
#[cfg(target_os = "linux")]
std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
#[cfg(target_os = "linux")]
std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
let builder = Builder::<tauri::Wry>::new().commands(collect_commands![
get_bootstrap_relays,
set_bootstrap_relays,
get_inbox_relays,
set_inbox_relays,
ensure_inbox_relays,
connect_inbox_relays,
disconnect_inbox_relays,
login,
create_account,
import_account,
connect_account,
delete_account,
reset_password,
get_accounts,
get_current_account,
get_metadata,
get_contact_list,
get_chats,
get_chat_messages,
send_message,
]);
let builder = Builder::<tauri::Wry>::new().commands(collect_commands![
get_bootstrap_relays,
set_bootstrap_relays,
get_inbox_relays,
set_inbox_relays,
ensure_inbox_relays,
connect_inbox_relays,
disconnect_inbox_relays,
login,
create_account,
import_account,
connect_account,
delete_account,
reset_password,
get_accounts,
get_current_account,
get_metadata,
get_contact_list,
get_chats,
get_chat_messages,
send_message,
]);
#[cfg(debug_assertions)]
builder
.export(Typescript::default(), "../src/commands.ts")
.expect("Failed to export typescript bindings");
#[cfg(debug_assertions)]
builder
.export(Typescript::default(), "../src/commands.ts")
.expect("Failed to export typescript bindings");
tauri::Builder::default()
#[cfg(debug_assertions)]
let tauri_builder = tauri::Builder::default().plugin(tauri_plugin_devtools::init());
#[cfg(not(debug_assertions))]
let tauri_builder = tauri::Builder::default();
tauri_builder
.invoke_handler(builder.invoke_handler())
.setup(move |app| {
let handle = app.handle();
let handle_clone = handle.clone();
let handle_clone_child = handle_clone.clone();
// Setup tray icon
let _ = setup_tray_icon(handle);
#[cfg(not(target_os = "linux"))]
@@ -88,11 +114,10 @@ fn main() {
let _ = fs::create_dir_all(&dir);
// Setup database
let database = NostrLMDB::open(dir.join("nostr-lmdb"))
.expect("Error: cannot create database.");
let database = NostrLMDB::open(dir.join("nostr-lmdb")).expect("Error: cannot create database.");
// Setup nostr client
let opts = Options::new().gossip(true).autoconnect(true);
let opts = Options::new().gossip(true).automatic_authentication(false).max_avg_latency(Duration::from_millis(500));
let client = ClientBuilder::default().opts(opts).database(database).build();
// Add bootstrap relays
@@ -123,15 +148,128 @@ fn main() {
}
}
let _ = client.add_discovery_relay("wss://user.kindpag.es/").await;
// Connect
client.connect().await;
client
});
// Create global state
app.manage(Nostr { client, inbox_relays: Mutex::new(HashMap::new()) });
app.manage(Nostr {
client,
queue: RwLock::new(HashSet::new()),
inbox_relays: RwLock::new(HashMap::new()),
});
// Listen for request metadata
app.listen_any("request_metadata", move |event| {
let payload = event.payload();
let parsed_payload: Payload = serde_json::from_str(payload).expect("Parse failed");
let handle = handle_clone.to_owned();
tauri::async_runtime::spawn(async move {
let state = handle.state::<Nostr>();
let client = &state.client;
if let Ok(public_key) = PublicKey::parse(parsed_payload.id) {
let mut write_queue = state.queue.write().await;
write_queue.insert(public_key);
};
// Wait for [QUEUE_DELAY]
sleep(Duration::from_millis(QUEUE_DELAY)).await;
let read_queue = state.queue.read().await;
if !read_queue.is_empty() {
let authors: HashSet<PublicKey> = read_queue.iter().copied().collect();
let filter = Filter::new().authors(authors).kind(Kind::Metadata).limit(200);
let opts = SubscribeAutoCloseOptions::default()
.filter(FilterOptions::WaitDurationAfterEOSE(Duration::from_secs(2)));
// Drop queue, you don't need it at this time anymore
drop(read_queue);
// Clear queue
let mut write_queue = state.queue.write().await;
write_queue.clear();
if let Err(e) = client.subscribe(vec![filter], Some(opts)).await {
println!("Subscribe error: {}", e);
}
}
});
});
// Run a thread for handle notification
tauri::async_runtime::spawn(async move {
let handle = handle_clone_child.to_owned();
let state = handle.state::<Nostr>();
let client = &state.client;
// Generate a fake sig for rumor event.
// TODO: Find better way to save unsigned event to database.
let fake_sig = Signature::from_str("f9e79d141c004977192d05a86f81ec7c585179c371f7350a5412d33575a2a356433f58e405c2296ed273e2fe0aafa25b641e39cc4e1f3f261ebf55bce0cbac83").unwrap();
let _ = client
.handle_notifications(|notification| async {
#[allow(clippy::collapsible_match)]
if let RelayPoolNotification::Message { message, .. } = notification {
if let RelayMessage::Event { event, .. } = message {
if event.kind == Kind::GiftWrap {
if let Ok(UnwrappedGift { rumor, sender }) =
client.unwrap_gift_wrap(&event).await
{
let mut rumor_clone = rumor.clone();
// Compute event id if not exist
rumor_clone.ensure_id();
let ev = Event::new(
rumor_clone.id.unwrap(), // unwrap() must be fine
rumor_clone.pubkey,
rumor_clone.created_at,
rumor_clone.kind,
rumor_clone.tags,
rumor_clone.content,
fake_sig,
);
// Save rumor to database to further query
if let Err(e) = client.database().save_event(&ev).await {
println!("[save event] error: {}", e)
}
// Emit new event to frontend
if let Err(e) = handle.emit(
"event",
EventPayload {
event: rumor.as_json(),
sender: sender.to_hex(),
},
) {
println!("[emit] error: {}", e)
}
}
} else if event.kind == Kind::Metadata {
if let Err(e) = handle.emit("metadata", event.as_json()) {
println!("Emit error: {}", e)
}
}
}
}
Ok(false)
})
.await;
});
Ok(())
})
.plugin(prevent_default())
.plugin(tauri_plugin_store::Builder::default().build())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_updater::Builder::new().build())
@@ -152,14 +290,14 @@ fn main() {
#[cfg(debug_assertions)]
fn prevent_default() -> tauri::plugin::TauriPlugin<tauri::Wry> {
use tauri_plugin_prevent_default::Flags;
use tauri_plugin_prevent_default::Flags;
tauri_plugin_prevent_default::Builder::new()
.with_flags(Flags::all().difference(Flags::CONTEXT_MENU))
.build()
tauri_plugin_prevent_default::Builder::new()
.with_flags(Flags::all().difference(Flags::CONTEXT_MENU))
.build()
}
#[cfg(not(debug_assertions))]
fn prevent_default() -> tauri::plugin::TauriPlugin<tauri::Wry> {
tauri_plugin_prevent_default::Builder::new().build()
tauri_plugin_prevent_default::Builder::new().build()
}

View File

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

View File

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

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 { readFile } from "@tauri-apps/plugin-fs";
import {
@@ -5,6 +10,7 @@ import {
requestPermission,
} from "@tauri-apps/plugin-notification";
import { relaunch } from "@tauri-apps/plugin-process";
import type { LazyStore } from "@tauri-apps/plugin-store";
import { check } from "@tauri-apps/plugin-updater";
import { type ClassValue, clsx } from "clsx";
import dayjs from "dayjs";
@@ -13,22 +19,6 @@ import updateLocale from "dayjs/plugin/updateLocale";
import { type NostrEvent, nip19 } from "nostr-tools";
import { twMerge } from "tailwind-merge";
dayjs.extend(relativeTime);
dayjs.extend(updateLocale);
dayjs.updateLocale("en", {
relativeTime: {
past: "%s",
s: "now",
m: "1m",
mm: "%dm",
h: "1h",
hh: "%dh",
d: "1d",
dd: "%dd",
},
});
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
@@ -52,6 +42,22 @@ export function npub(pubkey: string, len: number) {
}
export function ago(time: number) {
dayjs.extend(relativeTime);
dayjs.extend(updateLocale);
dayjs.updateLocale("en", {
relativeTime: {
past: "%s",
s: "now",
m: "1m",
mm: "%dm",
h: "1h",
hh: "%dh",
d: "1d",
dd: "%dd",
},
});
let formated: string;
const now = dayjs();
@@ -68,6 +74,22 @@ export function ago(time: number) {
}
export function time(time: number) {
dayjs.extend(relativeTime);
dayjs.extend(updateLocale);
dayjs.updateLocale("en", {
relativeTime: {
past: "%s",
s: "now",
m: "1m",
mm: "%dm",
h: "1h",
hh: "%dh",
d: "1d",
dd: "%dd",
},
});
const input = new Date(time * 1000);
const formattedTime = input.toLocaleTimeString([], {
hour: "2-digit",
@@ -187,3 +209,14 @@ export async function upload() {
return null;
}
}
export function newQueryStorage(
store: LazyStore,
): AsyncStorage<PersistedQuery> {
return {
getItem: async (key) => await store.get(key),
setItem: async (key, value) => await store.set(key, value),
removeItem: async (key) =>
(await store.delete(key)) as unknown as MaybePromise<void>,
};
}

View File

@@ -1,27 +1,13 @@
import { commands } from "@/commands";
import { useQuery } from "@tanstack/react-query";
import { type Metadata, useProfile } from "@/hooks/useProfile";
import { type ReactNode, createContext, useContext } from "react";
type Metadata = {
name?: string;
display_name?: string;
about?: string;
website?: string;
picture?: string;
banner?: string;
nip05?: string;
lud06?: string;
lud16?: string;
};
type UserContext = {
pubkey: string;
isLoading: boolean;
isError: boolean;
profile: Metadata | undefined;
isLoading: boolean;
};
const UserContext = createContext<UserContext>(null);
const UserContext = createContext<UserContext | null>(null);
export function UserProvider({
pubkey,
@@ -30,37 +16,10 @@ export function UserProvider({
pubkey: string;
children: ReactNode;
}) {
const {
isLoading,
isError,
data: profile,
} = useQuery({
queryKey: ["profile", pubkey],
queryFn: async () => {
try {
const normalizePubkey = pubkey
.replace("nostr:", "")
.replace(/[^\w\s]/gi, "");
const res = await commands.getMetadata(normalizePubkey);
if (res.status === "ok") {
return JSON.parse(res.data) as Metadata;
} else {
throw new Error(res.error);
}
} catch (e) {
throw new Error(String(e));
}
},
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
staleTime: Number.POSITIVE_INFINITY,
});
const { isLoading, profile } = useProfile(pubkey);
return (
<UserContext.Provider value={{ pubkey, profile, isError, isLoading }}>
<UserContext.Provider value={{ pubkey, profile, isLoading }}>
{children}
</UserContext.Provider>
);

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

View File

@@ -1,12 +1,12 @@
/* prettier-ignore-start */
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file is auto-generated by TanStack Router
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { createFileRoute } from '@tanstack/react-router'
@@ -38,26 +38,31 @@ const AccountLayoutChatsNewLazyImport = createFileRoute(
// Create/Update Routes
const AccountRoute = AccountImport.update({
id: '/$account',
path: '/$account',
getParentRoute: () => rootRoute,
} as any)
const ResetLazyRoute = ResetLazyImport.update({
id: '/reset',
path: '/reset',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/reset.lazy').then((d) => d.Route))
const NewLazyRoute = NewLazyImport.update({
id: '/new',
path: '/new',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/new.lazy').then((d) => d.Route))
const InboxRelaysRoute = InboxRelaysImport.update({
id: '/inbox-relays',
path: '/inbox-relays',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/inbox-relays.lazy').then((d) => d.Route))
const BootstrapRelaysRoute = BootstrapRelaysImport.update({
id: '/bootstrap-relays',
path: '/bootstrap-relays',
getParentRoute: () => rootRoute,
} as any).lazy(() =>
@@ -65,21 +70,25 @@ const BootstrapRelaysRoute = BootstrapRelaysImport.update({
)
const IndexRoute = IndexImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRoute,
} as any).lazy(() => import('./routes/index.lazy').then((d) => d.Route))
const AuthNewRoute = AuthNewImport.update({
id: '/auth/new',
path: '/auth/new',
getParentRoute: () => rootRoute,
} as any)
const AuthImportRoute = AuthImportImport.update({
id: '/auth/import',
path: '/auth/import',
getParentRoute: () => rootRoute,
} as any)
const AuthConnectRoute = AuthConnectImport.update({
id: '/auth/connect',
path: '/auth/connect',
getParentRoute: () => rootRoute,
} as any)
@@ -90,6 +99,7 @@ const AccountLayoutRoute = AccountLayoutImport.update({
} as any)
const AccountLayoutChatsLazyRoute = AccountLayoutChatsLazyImport.update({
id: '/chats',
path: '/chats',
getParentRoute: () => AccountLayoutRoute,
} as any).lazy(() =>
@@ -97,6 +107,7 @@ const AccountLayoutChatsLazyRoute = AccountLayoutChatsLazyImport.update({
)
const AccountLayoutContactsRoute = AccountLayoutContactsImport.update({
id: '/contacts',
path: '/contacts',
getParentRoute: () => AccountLayoutRoute,
} as any).lazy(() =>
@@ -104,6 +115,7 @@ const AccountLayoutContactsRoute = AccountLayoutContactsImport.update({
)
const AccountLayoutChatsNewLazyRoute = AccountLayoutChatsNewLazyImport.update({
id: '/new',
path: '/new',
getParentRoute: () => AccountLayoutChatsLazyRoute,
} as any).lazy(() =>
@@ -111,6 +123,7 @@ const AccountLayoutChatsNewLazyRoute = AccountLayoutChatsNewLazyImport.update({
)
const AccountLayoutChatsIdRoute = AccountLayoutChatsIdImport.update({
id: '/$id',
path: '/$id',
getParentRoute: () => AccountLayoutChatsLazyRoute,
} as any).lazy(() =>
@@ -393,8 +406,6 @@ export const routeTree = rootRoute
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
/* prettier-ignore-end */
/* ROUTE_MANIFEST_START
{
"routes": {

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,14 @@
import { cn } from "@/commons";
import type { Metadata } from "@/hooks/useProfile";
import type { QueryClient } from "@tanstack/react-query";
import { Outlet, createRootRouteWithContext } from "@tanstack/react-router";
import { getCurrentWindow } from "@tauri-apps/api/window";
import type { OsType } from "@tauri-apps/plugin-os";
import type { LRUCache } from "lru-cache";
import type { NostrEvent } from "nostr-tools";
import { useEffect } from "react";
interface RouterContext {
queryClient: QueryClient;
chatManager: LRUCache<string, string, unknown>;
platform: OsType;
}
@@ -15,7 +17,29 @@ export const Route = createRootRouteWithContext<RouterContext>()({
});
function RootComponent() {
const { platform } = Route.useRouteContext();
const { platform, queryClient } = Route.useRouteContext();
useEffect(() => {
const unlisten = getCurrentWindow().listen<string>(
"metadata",
async (data) => {
const event: NostrEvent = JSON.parse(data.payload);
const metadata: Metadata = JSON.parse(event.content);
// Update query cache
queryClient.setQueryData(["profile", event.pubkey], () => metadata);
// Reset query cache
await queryClient.invalidateQueries({
queryKey: ["profile", event.pubkey],
});
},
);
return () => {
unlisten.then((f) => f());
};
}, []);
return (
<div

View File

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