Continue redesign for the v1 stable release #5

Merged
reya merged 11 commits from redesign-2 into master 2026-02-12 08:32:17 +00:00
17 changed files with 1081 additions and 872 deletions
Showing only changes of commit 253d04f988 - Show all commits

406
Cargo.lock generated
View File

@@ -85,6 +85,12 @@ dependencies = [
"equator", "equator",
] ]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]] [[package]]
name = "android_system_properties" name = "android_system_properties"
version = "0.1.5" version = "0.1.5"
@@ -530,6 +536,15 @@ dependencies = [
"thiserror 2.0.18", "thiserror 2.0.18",
] ]
[[package]]
name = "atoi"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "atomic" name = "atomic"
version = "0.5.3" version = "0.5.3"
@@ -1349,6 +1364,12 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]] [[package]]
name = "const-random" name = "const-random"
version = "0.1.18" version = "0.1.18"
@@ -1599,6 +1620,21 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crc"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.5.0" version = "1.5.0"
@@ -1693,6 +1729,17 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204"
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid",
"pem-rfc7468",
"zeroize",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "0.99.20" version = "0.99.20"
@@ -1741,6 +1788,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"const-oid",
"crypto-common", "crypto-common",
"subtle", "subtle",
] ]
@@ -1845,6 +1893,12 @@ dependencies = [
"ui", "ui",
] ]
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]] [[package]]
name = "downcast-rs" name = "downcast-rs"
version = "1.2.1" version = "1.2.1"
@@ -1904,6 +1958,9 @@ name = "either"
version = "1.15.0" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "embed-resource" name = "embed-resource"
@@ -2008,7 +2065,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -2021,6 +2078,17 @@ dependencies = [
"svg_fmt", "svg_fmt",
] ]
[[package]]
name = "etcetera"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
dependencies = [
"cfg-if",
"home",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "euclid" name = "euclid"
version = "0.22.13" version = "0.22.13"
@@ -2409,6 +2477,17 @@ dependencies = [
"futures-util", "futures-util",
] ]
[[package]]
name = "futures-intrusive"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
dependencies = [
"futures-core",
"lock_api",
"parking_lot",
]
[[package]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.31" version = "0.3.31"
@@ -2811,6 +2890,8 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [ dependencies = [
"allocator-api2",
"equivalent",
"foldhash", "foldhash",
] ]
@@ -2820,6 +2901,15 @@ version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "hashlink"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
"hashbrown 0.15.5",
]
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.4.1" version = "0.4.1"
@@ -3538,6 +3628,17 @@ dependencies = [
"redox_syscall 0.7.0", "redox_syscall 0.7.0",
] ]
[[package]]
name = "libsqlite3-sys"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
dependencies = [
"cc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "linicon" name = "linicon"
version = "2.3.0" version = "2.3.0"
@@ -3982,7 +4083,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]] [[package]]
name = "nostr" name = "nostr"
version = "0.44.1" version = "0.44.1"
source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7"
dependencies = [ dependencies = [
"aes", "aes",
"base64", "base64",
@@ -4007,7 +4108,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-connect" name = "nostr-connect"
version = "0.44.0" version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"futures-core", "futures-core",
@@ -4020,7 +4121,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-database" name = "nostr-database"
version = "0.44.0" version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7"
dependencies = [ dependencies = [
"btreecap", "btreecap",
"flatbuffers", "flatbuffers",
@@ -4032,27 +4133,26 @@ dependencies = [
[[package]] [[package]]
name = "nostr-gossip" name = "nostr-gossip"
version = "0.44.0" version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7"
dependencies = [ dependencies = [
"nostr", "nostr",
] ]
[[package]] [[package]]
name = "nostr-gossip-memory" name = "nostr-gossip-sqlite"
version = "0.44.0" version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7"
dependencies = [ dependencies = [
"indexmap",
"lru",
"nostr", "nostr",
"nostr-gossip", "nostr-gossip",
"sqlx",
"tokio", "tokio",
] ]
[[package]] [[package]]
name = "nostr-lmdb" name = "nostr-lmdb"
version = "0.44.0" version = "0.44.0"
source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"flume", "flume",
@@ -4066,7 +4166,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-sdk" name = "nostr-sdk"
version = "0.44.1" version = "0.44.1"
source = "git+https://github.com/rust-nostr/nostr#8b6a6d797f0a7b7fd9147860b3cb00bdd6fc92cd" source = "git+https://github.com/rust-nostr/nostr#9031e3684c661690a4c61865ac11d311456371e7"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"async-wsocket", "async-wsocket",
@@ -4097,7 +4197,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -4591,6 +4691,15 @@ dependencies = [
"hmac", "hmac",
] ]
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
dependencies = [
"base64ct",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@@ -4727,6 +4836,27 @@ dependencies = [
"futures-io", "futures-io",
] ]
[[package]]
name = "pkcs1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der",
"pkcs8",
"spki",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.32" version = "0.3.32"
@@ -5007,7 +5137,7 @@ dependencies = [
"once_cell", "once_cell",
"socket2", "socket2",
"tracing", "tracing",
"windows-sys 0.60.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -5421,6 +5551,26 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]]
name = "rsa"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
dependencies = [
"const-oid",
"digest",
"num-bigint-dig",
"num-integer",
"num-traits",
"pkcs1",
"pkcs8",
"rand_core 0.6.4",
"signature",
"spki",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "rust-embed" name = "rust-embed"
version = "8.11.0" version = "8.11.0"
@@ -5516,7 +5666,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.11.0", "linux-raw-sys 0.11.0",
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -6029,6 +6179,16 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest",
"rand_core 0.6.4",
]
[[package]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.8" version = "0.3.8"
@@ -6089,6 +6249,9 @@ name = "smallvec"
version = "1.15.1" version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "smol" name = "smol"
@@ -6150,6 +6313,204 @@ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
] ]
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "sqlx"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
dependencies = [
"sqlx-core",
"sqlx-macros",
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
]
[[package]]
name = "sqlx-core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
dependencies = [
"base64",
"bytes",
"crc",
"crossbeam-queue",
"either",
"event-listener 5.4.1",
"futures-core",
"futures-intrusive",
"futures-io",
"futures-util",
"hashbrown 0.15.5",
"hashlink",
"indexmap",
"log",
"memchr",
"once_cell",
"percent-encoding",
"serde",
"serde_json",
"sha2",
"smallvec",
"thiserror 2.0.18",
"tokio",
"tokio-stream",
"tracing",
"url",
]
[[package]]
name = "sqlx-macros"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
dependencies = [
"proc-macro2",
"quote",
"sqlx-core",
"sqlx-macros-core",
"syn 2.0.114",
]
[[package]]
name = "sqlx-macros-core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
dependencies = [
"dotenvy",
"either",
"heck 0.5.0",
"hex",
"once_cell",
"proc-macro2",
"quote",
"serde",
"serde_json",
"sha2",
"sqlx-core",
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
"syn 2.0.114",
"tokio",
"url",
]
[[package]]
name = "sqlx-mysql"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
dependencies = [
"atoi",
"base64",
"bitflags 2.10.0",
"byteorder",
"bytes",
"crc",
"digest",
"dotenvy",
"either",
"futures-channel",
"futures-core",
"futures-io",
"futures-util",
"generic-array",
"hex",
"hkdf",
"hmac",
"itoa",
"log",
"md-5",
"memchr",
"once_cell",
"percent-encoding",
"rand 0.8.5",
"rsa",
"serde",
"sha1",
"sha2",
"smallvec",
"sqlx-core",
"stringprep",
"thiserror 2.0.18",
"tracing",
"whoami",
]
[[package]]
name = "sqlx-postgres"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
dependencies = [
"atoi",
"base64",
"bitflags 2.10.0",
"byteorder",
"crc",
"dotenvy",
"etcetera",
"futures-channel",
"futures-core",
"futures-util",
"hex",
"hkdf",
"hmac",
"home",
"itoa",
"log",
"md-5",
"memchr",
"once_cell",
"rand 0.8.5",
"serde",
"serde_json",
"sha2",
"smallvec",
"sqlx-core",
"stringprep",
"thiserror 2.0.18",
"tracing",
"whoami",
]
[[package]]
name = "sqlx-sqlite"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
dependencies = [
"atoi",
"flume",
"futures-channel",
"futures-core",
"futures-executor",
"futures-intrusive",
"futures-util",
"libsqlite3-sys",
"log",
"percent-encoding",
"serde",
"serde_urlencoded",
"sqlx-core",
"thiserror 2.0.18",
"tracing",
"url",
]
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.1" version = "1.2.1"
@@ -6201,7 +6562,7 @@ dependencies = [
"gpui_tokio", "gpui_tokio",
"log", "log",
"nostr-connect", "nostr-connect",
"nostr-gossip-memory", "nostr-gossip-sqlite",
"nostr-lmdb", "nostr-lmdb",
"nostr-sdk", "nostr-sdk",
"petname", "petname",
@@ -6228,6 +6589,17 @@ dependencies = [
"float-cmp", "float-cmp",
] ]
[[package]]
name = "stringprep"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
dependencies = [
"unicode-bidi",
"unicode-normalization",
"unicode-properties",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
@@ -6546,7 +6918,7 @@ dependencies = [
"getrandom 0.3.4", "getrandom 0.3.4",
"once_cell", "once_cell",
"rustix 1.1.3", "rustix 1.1.3",
"windows-sys 0.61.2", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -7753,7 +8125,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.48.0",
] ]
[[package]] [[package]]

View File

@@ -19,7 +19,7 @@ reqwest_client = { git = "https://github.com/zed-industries/zed" }
nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" } nostr-lmdb = { git = "https://github.com/rust-nostr/nostr" }
nostr-connect = { git = "https://github.com/rust-nostr/nostr" } nostr-connect = { git = "https://github.com/rust-nostr/nostr" }
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96", "nip59", "nip49", "nip44" ] } nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96", "nip59", "nip49", "nip44" ] }
nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" } nostr-gossip-sqlite = { git = "https://github.com/rust-nostr/nostr" }
# Others # Others
anyhow = "1.0.44" anyhow = "1.0.44"

View File

@@ -98,7 +98,7 @@ impl ChatRegistry {
/// Create a new chat registry instance /// Create a new chat registry instance
fn new(cx: &mut Context<Self>) -> Self { fn new(cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let identity = nostr.read(cx).identity(); let nip17_state = nostr.read(cx).nip17_state();
let device = DeviceRegistry::global(cx); let device = DeviceRegistry::global(cx);
let device_signer = device.read(cx).device_signer.clone(); let device_signer = device.read(cx).device_signer.clone();
@@ -114,8 +114,8 @@ impl ChatRegistry {
subscriptions.push( subscriptions.push(
// Observe the identity // Observe the identity
cx.observe(&identity, |this, state, cx| { cx.observe(&nip17_state, |this, state, cx| {
if state.read(cx).messaging_relays_state() == RelayState::Set { if state.read(cx) == &RelayState::Configured {
// Handle nostr notifications // Handle nostr notifications
this.handle_notifications(cx); this.handle_notifications(cx);
// Track unwrapping progress // Track unwrapping progress
@@ -536,9 +536,9 @@ impl ChatRegistry {
} }
// Set this room is ongoing if the new message is from current user // Set this room is ongoing if the new message is from current user
if author == nostr.read(cx).identity().read(cx).public_key() { // if author == nostr.read(cx).identity().read(cx).public_key() {
this.set_ongoing(cx); // this.set_ongoing(cx);
} // }
// Emit the new message to the room // Emit the new message to the room
this.emit_message(message, cx); this.emit_message(message, cx);

View File

@@ -216,28 +216,6 @@ impl Room {
self.members.clone() self.members.clone()
} }
/// Returns the members of the room with their messaging relays
pub fn members_with_relays(&self, cx: &App) -> Task<Vec<(PublicKey, Vec<RelayUrl>)>> {
let nostr = NostrRegistry::global(cx);
let mut tasks = vec![];
for member in self.members.iter() {
let task = nostr.read(cx).messaging_relays(member, cx);
tasks.push((*member, task));
}
cx.background_spawn(async move {
let mut results = vec![];
for (public_key, task) in tasks.into_iter() {
let urls = task.await;
results.push((public_key, urls));
}
results
})
}
/// Checks if the room has more than two members (group) /// Checks if the room has more than two members (group)
pub fn is_group(&self) -> bool { pub fn is_group(&self) -> bool {
self.members.len() > 2 self.members.len() > 2
@@ -266,17 +244,7 @@ impl Room {
/// Display member is always different from the current user. /// Display member is always different from the current user.
pub fn display_member(&self, cx: &App) -> Person { pub fn display_member(&self, cx: &App) -> Person {
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
let nostr = NostrRegistry::global(cx); persons.read(cx).get(&self.members[0], cx)
let public_key = nostr.read(cx).identity().read(cx).public_key();
let target_member = self
.members
.iter()
.find(|&member| member != &public_key)
.or_else(|| self.members.first())
.expect("Room should have at least one member");
persons.read(cx).get(target_member, cx)
} }
/// Merge the names of the first two members of the room. /// Merge the names of the first two members of the room.
@@ -377,68 +345,79 @@ impl Room {
}) })
} }
/// Create a new message event (unsigned) /// Create a new unsigned message event
pub fn create_message(&self, content: &str, replies: &[EventId], cx: &App) -> UnsignedEvent { pub fn create_message(
&self,
content: &str,
replies: Vec<EventId>,
cx: &App,
) -> Task<Result<UnsignedEvent, Error>> {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client();
// Get current user
let public_key = nostr.read(cx).identity().read(cx).public_key();
// Get room's subject
let subject = self.subject.clone(); let subject = self.subject.clone();
let content = content.to_string();
let mut tags = vec![]; let mut member_and_relay_hints = HashMap::new();
// Add receivers // Populate the hashmap with member and relay hint tasks
//
// NOTE: current user will be removed from the list of receivers
for member in self.members.iter() { for member in self.members.iter() {
// Get relay hint if available let hint = nostr.read(cx).relay_hint(member, cx);
let relay_url = nostr.read(cx).relay_hint(member, cx); member_and_relay_hints.insert(member.to_owned(), hint);
// Construct a public key tag with relay hint
let tag = TagStandard::PublicKey {
public_key: member.to_owned(),
relay_url,
alias: None,
uppercase: false,
};
tags.push(Tag::from_standardized_without_cell(tag));
} }
// Add subject tag if it's present cx.background_spawn(async move {
if let Some(value) = subject { let signer = client.signer().context("Signer not found")?;
tags.push(Tag::from_standardized_without_cell(TagStandard::Subject( let public_key = signer.get_public_key().await?;
value.to_string(),
)));
}
// Add reply/quote tag // List of event tags for each receiver
if replies.len() == 1 { let mut tags = vec![];
tags.push(Tag::event(replies[0]))
} else { for (member, task) in member_and_relay_hints.into_iter() {
for id in replies { // Skip current user
let tag = TagStandard::Quote { if member == public_key {
event_id: id.to_owned(), continue;
relay_url: None, }
public_key: None,
// Get relay hint if available
let relay_url = task.await;
// Construct a public key tag with relay hint
let tag = TagStandard::PublicKey {
public_key: member,
relay_url,
alias: None,
uppercase: false,
}; };
tags.push(Tag::from_standardized_without_cell(tag))
tags.push(Tag::from_standardized_without_cell(tag));
} }
}
// Construct a direct message event // Add subject tag if present
// if let Some(value) = subject {
// WARNING: never sign and send this event to relays tags.push(Tag::from_standardized_without_cell(TagStandard::Subject(
let mut event = EventBuilder::new(Kind::PrivateDirectMessage, content) value.to_string(),
.tags(tags) )));
.build(public_key); }
// Ensure the event id has been generated // Add all reply tags
event.ensure_id(); for id in replies {
tags.push(Tag::event(id))
}
event // Construct a direct message event
//
// WARNING: never sign and send this event to relays
// TODO
let mut event = EventBuilder::new(Kind::PrivateDirectMessage, content)
.tags(tags)
.build(Keys::generate().public_key());
// Ensure the event ID has been generated
event.ensure_id();
Ok(event)
})
} }
/// Create a task to send a message to all room members /// Create a task to send a message to all room members
@@ -450,46 +429,27 @@ impl Room {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
// Get current user's public key and relays let mut members = self.members();
let current_user = nostr.read(cx).identity().read(cx).public_key();
let current_user_relays = nostr.read(cx).messaging_relays(&current_user, cx);
let rumor = rumor.to_owned(); let rumor = rumor.to_owned();
// Get all members and their messaging relays
let task = self.members_with_relays(cx);
cx.background_spawn(async move { cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?; let signer = client.signer().context("Signer not found")?;
let current_user_relays = current_user_relays.await; let current_user = signer.get_public_key().await?;
let mut members = task.await;
// Remove the current user's public key from the list of receivers // Remove the current user's public key from the list of receivers
// the current user will be handled separately // the current user will be handled separately
members.retain(|(this, _)| this != &current_user); members.retain(|this| this != &current_user);
// Collect the send reports // Collect the send reports
let mut reports: Vec<SendReport> = vec![]; let mut reports: Vec<SendReport> = vec![];
for (receiver, relays) in members.into_iter() { for receiver in members.into_iter() {
// Check if there are any relays to send the message to
if relays.is_empty() {
reports.push(SendReport::new(receiver).relays_not_found());
continue;
}
// Ensure relay connection
for url in relays.iter() {
client.add_relay(url).await?;
client.connect_relay(url).await?;
}
// Construct the gift wrap event // Construct the gift wrap event
let event = let event =
EventBuilder::gift_wrap(signer, &receiver, rumor.clone(), vec![]).await?; EventBuilder::gift_wrap(signer, &receiver, rumor.clone(), vec![]).await?;
// Send the gift wrap event to the messaging relays // Send the gift wrap event to the messaging relays
match client.send_event(&event).to(&relays).await { match client.send_event(&event).to_nip17().await {
Ok(output) => { Ok(output) => {
let id = output.id().to_owned(); let id = output.id().to_owned();
let auth = output.failed.iter().any(|(_, s)| s.starts_with("auth-")); let auth = output.failed.iter().any(|(_, s)| s.starts_with("auth-"));
@@ -531,20 +491,8 @@ impl Room {
// Only send a backup message to current user if sent successfully to others // Only send a backup message to current user if sent successfully to others
if reports.iter().all(|r| r.is_sent_success()) { if reports.iter().all(|r| r.is_sent_success()) {
// Check if there are any relays to send the event to
if current_user_relays.is_empty() {
reports.push(SendReport::new(current_user).relays_not_found());
return Ok(reports);
}
// Ensure relay connection
for url in current_user_relays.iter() {
client.add_relay(url).await?;
client.connect_relay(url).await?;
}
// Send the event to the messaging relays // Send the event to the messaging relays
match client.send_event_to(current_user_relays, &event).await { match client.send_event(&event).to_nip17().await {
Ok(output) => { Ok(output) => {
reports.push(SendReport::new(current_user).status(output)); reports.push(SendReport::new(current_user).status(output));
} }

View File

@@ -1,7 +1,7 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::time::Duration;
pub use actions::*; pub use actions::*;
use anyhow::Error;
use chat::{Message, RenderedMessage, Room, RoomEvent, RoomKind, SendReport}; use chat::{Message, RenderedMessage, Room, RoomEvent, RoomKind, SendReport};
use common::{nip96_upload, RenderedTimestamp}; use common::{nip96_upload, RenderedTimestamp};
use dock::panel::{Panel, PanelEvent}; use dock::panel::{Panel, PanelEvent};
@@ -244,27 +244,21 @@ impl ChatPanel {
return; return;
} }
// Get the current room entity
let Some(room) = self.room.upgrade().map(|this| this.read(cx)) else {
return;
};
// Get replies_to if it's present // Get replies_to if it's present
let replies: Vec<EventId> = self.replies_to.read(cx).iter().copied().collect(); let replies: Vec<EventId> = self.replies_to.read(cx).iter().copied().collect();
// Create a temporary message for optimistic update // Get a task to create temporary message for optimistic update
let rumor = room.create_message(&content, replies.as_ref(), cx); let Ok(get_rumor) = self
let rumor_id = rumor.id.unwrap(); .room
.read_with(cx, |this, cx| this.create_message(&content, replies, cx))
// Create a task for sending the message in the background else {
let send_message = room.send_message(&rumor, cx); return;
};
// Optimistically update message list // Optimistically update message list
cx.spawn_in(window, async move |this, cx| { let task: Task<Result<(), Error>> = cx.spawn_in(window, async move |this, cx| {
// Wait for the delay let mut rumor = get_rumor.await?;
cx.background_executor() let rumor_id = rumor.id();
.timer(Duration::from_millis(100))
.await;
// Update the message list and reset the states // Update the message list and reset the states
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
@@ -280,43 +274,50 @@ impl ChatPanel {
// Update the message list // Update the message list
this.insert_message(&rumor, true, cx); this.insert_message(&rumor, true, cx);
})
.ok();
})
.detach();
self.tasks.push(cx.spawn_in(window, async move |this, cx| { if let Ok(task) = this
let result = send_message.await; .room
.read_with(cx, |this, cx| this.send_message(&rumor, cx))
{
this.tasks.push(cx.spawn_in(window, async move |this, cx| {
let result = task.await;
this.update_in(cx, |this, window, cx| { this.update_in(cx, |this, window, cx| {
match result { match result {
Ok(reports) => { Ok(reports) => {
// Update room's status // Update room's status
this.room this.room
.update(cx, |this, cx| { .update(cx, |this, cx| {
if this.kind != RoomKind::Ongoing { if this.kind != RoomKind::Ongoing {
// Update the room kind to ongoing, // Update the room kind to ongoing,
// but keep the room kind if send failed // but keep the room kind if send failed
if reports.iter().all(|r| !r.is_sent_success()) { if reports.iter().all(|r| !r.is_sent_success()) {
this.kind = RoomKind::Ongoing; this.kind = RoomKind::Ongoing;
cx.notify(); cx.notify();
} }
}
})
.ok();
// Insert the sent reports
this.reports_by_id.insert(rumor_id, reports);
cx.notify();
} }
}) Err(e) => {
.ok(); window.push_notification(e.to_string(), cx);
}
// Insert the sent reports }
this.reports_by_id.insert(rumor_id, reports); })
.ok();
cx.notify(); }))
}
Err(e) => {
window.push_notification(e.to_string(), cx);
}
} }
}) })?;
.ok();
})); Ok(())
});
task.detach();
} }
/// Insert a message into the chat panel /// Insert a message into the chat panel

View File

@@ -169,7 +169,6 @@ impl CommandBar {
fn search(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn search(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let identity = nostr.read(cx).identity();
let query = self.find_input.read(cx).value(); let query = self.find_input.read(cx).value();
// Return if the query is empty // Return if the query is empty
@@ -191,7 +190,7 @@ impl CommandBar {
// Block the input until the search completes // Block the input until the search completes
self.set_finding(true, window, cx); self.set_finding(true, window, cx);
let find_users = if identity.read(cx).owned { let find_users = if nostr.read(cx).owned_signer() {
nostr.read(cx).wot_search(&query, cx) nostr.read(cx).wot_search(&query, cx)
} else { } else {
nostr.read(cx).search(&query, cx) nostr.read(cx).search(&query, cx)
@@ -245,17 +244,28 @@ impl CommandBar {
fn create(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn create(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let chat = ChatRegistry::global(cx); let chat = ChatRegistry::global(cx);
let nostr = NostrRegistry::global(cx); let async_chat = chat.downgrade();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let nostr = NostrRegistry::global(cx);
let signer_pkey = nostr.read(cx).signer_pkey(cx);
// Get all selected public keys
let receivers = self.selected(cx); let receivers = self.selected(cx);
chat.update(cx, |this, cx| { let task: Task<Result<(), Error>> = cx.spawn_in(window, async move |_this, cx| {
let room = cx.new(|_| Room::new(public_key, receivers)); let public_key = signer_pkey.await?;
this.emit_room(room.downgrade(), cx);
async_chat.update_in(cx, |this, window, cx| {
let room = cx.new(|_| Room::new(public_key, receivers));
this.emit_room(room.downgrade(), cx);
window.close_modal(cx);
})?;
Ok(())
}); });
window.close_modal(cx); task.detach();
} }
fn select(&mut self, pkey: PublicKey, cx: &mut Context<Self>) { fn select(&mut self, pkey: PublicKey, cx: &mut Context<Self>) {

View File

@@ -29,6 +29,26 @@ impl GreeterPanel {
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
} }
} }
fn add_profile_panel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx);
let signer_pkey = nostr.read(cx).signer_pkey(cx);
cx.spawn_in(window, async move |_this, cx| {
if let Ok(public_key) = signer_pkey.await {
cx.update(|window, cx| {
Workspace::add_panel(
profile::init(public_key, window, cx),
DockPlacement::Center,
window,
cx,
);
})
.ok();
}
})
.detach();
}
} }
impl Panel for GreeterPanel { impl Panel for GreeterPanel {
@@ -62,12 +82,11 @@ impl Render for GreeterPanel {
const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr."; const DESCRIPTION: &str = "Chat Freely, Stay Private on Nostr.";
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let identity = nostr.read(cx).identity(); let nip65_state = nostr.read(cx).nip65_state();
let nip17_state = nostr.read(cx).nip17_state();
let relay_list_state = identity.read(cx).relay_list_state(); let required_actions = nip65_state.read(cx) == &RelayState::NotConfigured
let messaging_relay_state = identity.read(cx).messaging_relays_state(); || nip17_state.read(cx) == &RelayState::NotConfigured;
let required_actions =
relay_list_state == RelayState::NotSet || messaging_relay_state == RelayState::NotSet;
h_flex() h_flex()
.size_full() .size_full()
@@ -128,7 +147,7 @@ impl Render for GreeterPanel {
v_flex() v_flex()
.gap_2() .gap_2()
.w_full() .w_full()
.when(relay_list_state == RelayState::NotSet, |this| { .when(nip65_state.read(cx).not_configured(), |this| {
this.child( this.child(
Button::new("relaylist") Button::new("relaylist")
.icon(Icon::new(IconName::Relay)) .icon(Icon::new(IconName::Relay))
@@ -146,31 +165,28 @@ impl Render for GreeterPanel {
}), }),
) )
}) })
.when( .when(nip17_state.read(cx).not_configured(), |this| {
messaging_relay_state == RelayState::NotSet, this.child(
|this| { Button::new("import")
this.child( .icon(Icon::new(IconName::Relay))
Button::new("import") .label("Set up messaging relays")
.icon(Icon::new(IconName::Relay)) .ghost()
.label("Set up messaging relays") .small()
.ghost() .no_center()
.small() .on_click(move |_ev, window, cx| {
.no_center() Workspace::add_panel(
.on_click(move |_ev, window, cx| { messaging_relays::init(window, cx),
Workspace::add_panel( DockPlacement::Center,
messaging_relays::init(window, cx), window,
DockPlacement::Center, cx,
window, );
cx, }),
); )
}), }),
)
},
),
), ),
) )
}) })
.when(!identity.read(cx).owned, |this| { .when(!nostr.read(cx).owned_signer(), |this| {
this.child( this.child(
v_flex() v_flex()
.gap_2() .gap_2()
@@ -257,14 +273,9 @@ impl Render for GreeterPanel {
.ghost() .ghost()
.small() .small()
.no_center() .no_center()
.on_click(move |_ev, window, cx| { .on_click(cx.listener(move |this, _ev, window, cx| {
Workspace::add_panel( this.add_profile_panel(window, cx)
profile::init(window, cx), })),
DockPlacement::Center,
window,
cx,
);
}),
) )
.child( .child(
Button::new("invite") Button::new("invite")

View File

@@ -156,29 +156,20 @@ impl MessagingRelayPanel {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let write_relays = nostr.read(cx).write_relays(&public_key, cx); let tags: Vec<Tag> = self
let relays = self.relays.clone(); .relays
.iter()
.map(|relay| Tag::relay(relay.clone()))
.collect();
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let urls = write_relays.await; // Construct nip17 event builder
let tags: Vec<Tag> = relays
.iter()
.map(|relay| Tag::relay(relay.clone()))
.collect();
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags); let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags);
let event = client.sign_event_builder(builder).await?; let event = client.sign_event_builder(builder).await?;
// Set messaging relays // Set messaging relays
client.send_event(&event).to(urls).await?; client.send_event(&event).to_nip65().await?;
// Connect to messaging relays
for relay in relays.iter() {
client.add_relay(relay).await.ok();
client.connect_relay(relay).await.ok();
}
Ok(()) Ok(())
}); });

View File

@@ -22,8 +22,8 @@ use ui::input::{InputState, TextInput};
use ui::notification::Notification; use ui::notification::Notification;
use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension}; use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension};
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ProfilePanel> { pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity<ProfilePanel> {
cx.new(|cx| ProfilePanel::new(window, cx)) cx.new(|cx| ProfilePanel::new(public_key, window, cx))
} }
#[derive(Debug)] #[derive(Debug)]
@@ -31,6 +31,9 @@ pub struct ProfilePanel {
name: SharedString, name: SharedString,
focus_handle: FocusHandle, focus_handle: FocusHandle,
/// User's public key
public_key: PublicKey,
/// User's name text input /// User's name text input
name_input: Entity<InputState>, name_input: Entity<InputState>,
@@ -51,13 +54,10 @@ pub struct ProfilePanel {
} }
impl ProfilePanel { impl ProfilePanel {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { fn new(public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) -> Self {
let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice")); let name_input = cx.new(|cx| InputState::new(window, cx).placeholder("Alice"));
let website_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me")); let website_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me"));
// Hidden input for avatar url
let avatar_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me/a.jpg")); let avatar_input = cx.new(|cx| InputState::new(window, cx).placeholder("alice.me/a.jpg"));
// Use multi-line input for bio // Use multi-line input for bio
let bio_input = cx.new(|cx| { let bio_input = cx.new(|cx| {
InputState::new(window, cx) InputState::new(window, cx)
@@ -66,13 +66,10 @@ impl ProfilePanel {
.placeholder("A short introduce about you.") .placeholder("A short introduce about you.")
}); });
// Get user's profile and update inputs
cx.defer_in(window, move |this, window, cx| { cx.defer_in(window, move |this, window, cx| {
let nostr = NostrRegistry::global(cx);
let public_key = nostr.read(cx).identity().read(cx).public_key();
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&public_key, cx); let profile = persons.read(cx).get(&public_key, cx);
// Set all input's values with current profile // Set all input's values with current profile
this.set_profile(profile, window, cx); this.set_profile(profile, window, cx);
}); });
@@ -80,6 +77,7 @@ impl ProfilePanel {
Self { Self {
name: "Update Profile".into(), name: "Update Profile".into(),
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
public_key,
name_input, name_input,
avatar_input, avatar_input,
bio_input, bio_input,
@@ -209,7 +207,7 @@ impl ProfilePanel {
fn set_metadata(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn set_metadata(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let public_key = nostr.read(cx).identity().read(cx).public_key(); let public_key = self.public_key;
// Get the old metadata // Get the old metadata
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
@@ -289,9 +287,7 @@ impl Focusable for ProfilePanel {
impl Render for ProfilePanel { impl Render for ProfilePanel {
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
let nostr = NostrRegistry::global(cx); let shorten_pkey = SharedString::from(shorten_pubkey(self.public_key, 8));
let public_key = nostr.read(cx).identity().read(cx).public_key();
let shorten_pkey = SharedString::from(shorten_pubkey(public_key, 8));
// Get the avatar // Get the avatar
let avatar_input = self.avatar_input.read(cx).value(); let avatar_input = self.avatar_input.read(cx).value();
@@ -390,7 +386,7 @@ impl Render for ProfilePanel {
.ghost() .ghost()
.on_click(cx.listener(move |this, _ev, window, cx| { .on_click(cx.listener(move |this, _ev, window, cx| {
this.copy( this.copy(
public_key.to_bech32().unwrap(), this.public_key.to_bech32().unwrap(),
window, window,
cx, cx,
); );

View File

@@ -9,10 +9,11 @@ use gpui::{
div, rems, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, div, rems, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement,
ParentElement, Render, SharedString, Styled, Subscription, Window, ParentElement, Render, SharedString, Styled, Subscription, Window,
}; };
use nostr_sdk::prelude::*;
use person::PersonRegistry; use person::PersonRegistry;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use state::NostrRegistry; use state::NostrRegistry;
use theme::{ActiveTheme, Theme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT}; use theme::{ActiveTheme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT};
use titlebar::TitleBar; use titlebar::TitleBar;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::{h_flex, v_flex, Icon, IconName, Root, Sizable, WindowExtension}; use ui::{h_flex, v_flex, Icon, IconName, Root, Sizable, WindowExtension};
@@ -35,6 +36,9 @@ pub struct Workspace {
/// App's Command Bar /// App's Command Bar
command_bar: Entity<CommandBar>, command_bar: Entity<CommandBar>,
/// Current User
current_user: Entity<Option<PublicKey>>,
/// Event subscriptions /// Event subscriptions
_subscriptions: SmallVec<[Subscription; 3]>, _subscriptions: SmallVec<[Subscription; 3]>,
} }
@@ -42,20 +46,23 @@ pub struct Workspace {
impl Workspace { impl Workspace {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let chat = ChatRegistry::global(cx); let chat = ChatRegistry::global(cx);
let current_user = cx.new(|_| None);
let nostr = NostrRegistry::global(cx);
let nip65_state = nostr.read(cx).nip65_state();
// Titlebar
let titlebar = cx.new(|_| TitleBar::new()); let titlebar = cx.new(|_| TitleBar::new());
// Command bar
let command_bar = cx.new(|cx| CommandBar::new(window, cx)); let command_bar = cx.new(|cx| CommandBar::new(window, cx));
// Dock
let dock = let dock =
cx.new(|cx| DockArea::new(window, cx).panel_style(dock::panel::PanelStyle::TabBar)); cx.new(|cx| DockArea::new(window, cx).panel_style(dock::panel::PanelStyle::TabBar));
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
subscriptions.push(
// Automatically sync theme with system appearance
window.observe_window_appearance(|window, cx| {
Theme::sync_system_appearance(Some(window), cx);
}),
);
subscriptions.push( subscriptions.push(
// Observe all events emitted by the chat registry // Observe all events emitted by the chat registry
cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| { cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| {
@@ -100,6 +107,15 @@ impl Workspace {
}), }),
); );
subscriptions.push(
// Observe the NIP-65 state
cx.observe(&nip65_state, move |this, state, cx| {
if state.read(cx).idle() {
this.get_current_user(cx);
}
}),
);
// Set the default layout for app's dock // Set the default layout for app's dock
cx.defer_in(window, |this, window, cx| { cx.defer_in(window, |this, window, cx| {
this.set_layout(window, cx); this.set_layout(window, cx);
@@ -109,6 +125,7 @@ impl Workspace {
titlebar, titlebar,
dock, dock,
command_bar, command_bar,
current_user,
_subscriptions: subscriptions, _subscriptions: subscriptions,
} }
} }
@@ -173,18 +190,35 @@ impl Workspace {
}); });
} }
fn titlebar_left(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement { fn get_current_user(&self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let identity = nostr.read(cx).identity(); let client = nostr.read(cx).client();
let current_user = self.current_user.downgrade();
cx.spawn(async move |_this, cx| {
if let Some(signer) = client.signer() {
if let Ok(public_key) = signer.get_public_key().await {
current_user
.update(cx, |this, cx| {
*this = Some(public_key);
cx.notify();
})
.ok();
}
}
})
.detach();
}
fn titlebar_left(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
h_flex() h_flex()
.h(TITLEBAR_HEIGHT) .h(TITLEBAR_HEIGHT)
.flex_1() .flex_1()
.justify_between() .justify_between()
.gap_2() .gap_2()
.when_some(identity.read(cx).public_key, |this, public_key| { .when_some(self.current_user.read(cx).as_ref(), |this, public_key| {
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&public_key, cx); let profile = persons.read(cx).get(public_key, cx);
this.child( this.child(
h_flex() h_flex()

View File

@@ -40,7 +40,7 @@ pub struct DeviceRegistry {
tasks: Vec<Task<Result<(), Error>>>, tasks: Vec<Task<Result<(), Error>>>,
/// Subscriptions /// Subscriptions
_subscriptions: SmallVec<[Subscription; 1]>, _subscriptions: SmallVec<[Subscription; 2]>,
} }
impl DeviceRegistry { impl DeviceRegistry {
@@ -58,7 +58,8 @@ impl DeviceRegistry {
fn new(cx: &mut Context<Self>) -> Self { fn new(cx: &mut Context<Self>) -> Self {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let identity = nostr.read(cx).identity(); let nip65_state = nostr.read(cx).nip65_state();
let nip17_state = nostr.read(cx).nip17_state();
let device_signer = cx.new(|_| None); let device_signer = cx.new(|_| None);
let requests = cx.new(|_| HashSet::default()); let requests = cx.new(|_| HashSet::default());
@@ -70,21 +71,26 @@ impl DeviceRegistry {
let mut tasks = vec![]; let mut tasks = vec![];
subscriptions.push( subscriptions.push(
// Observe the identity entity // Observe the NIP-65 state
cx.observe(&identity, |this, state, cx| { cx.observe(&nip65_state, |this, state, cx| {
match state.read(cx).relay_list_state() { match state.read(cx) {
RelayState::Initial => { RelayState::Idle => {
this.reset(cx); this.reset(cx);
} }
RelayState::Set => { RelayState::Configured => {
this.get_announcement(cx); this.get_announcement(cx);
if state.read(cx).messaging_relays_state() == RelayState::Set {
this.get_messages(cx);
}
} }
_ => {} _ => {}
} };
}),
);
subscriptions.push(
// Observe the NIP-17 state
cx.observe(&nip17_state, |this, state, cx| {
if state.read(cx) == &RelayState::Configured {
this.get_messages(cx);
};
}), }),
); );
@@ -265,29 +271,26 @@ impl DeviceRegistry {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let device_signer = self.device_signer.read(cx).clone(); let device_signer = self.device_signer.read(cx).clone();
let messaging_relays = nostr.read(cx).messaging_relays(cx);
let public_key = nostr.read(cx).identity().read(cx).public_key(); let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let messaging_relays = nostr.read(cx).messaging_relays(&public_key, cx);
cx.background_spawn(async move {
let urls = messaging_relays.await; let urls = messaging_relays.await;
let user_signer = client.signer().context("Signer not found")?;
let public_key = user_signer.get_public_key().await?;
// Get messages with dekey // Get messages with dekey
if let Some(signer) = device_signer.as_ref() { if let Some(signer) = device_signer.as_ref() {
if let Ok(pkey) = signer.get_public_key().await { let device_pkey = signer.get_public_key().await?;
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(pkey); let filter = Filter::new().kind(Kind::GiftWrap).pubkey(device_pkey);
let id = SubscriptionId::new(DEVICE_GIFTWRAP); let id = SubscriptionId::new(DEVICE_GIFTWRAP);
// Construct target for subscription // Construct target for subscription
let target = urls let target = urls
.iter() .iter()
.map(|relay| (relay, vec![filter.clone()])) .map(|relay| (relay, vec![filter.clone()]))
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
if let Err(e) = client.subscribe(target).with_id(id).await { client.subscribe(target).with_id(id).await?;
log::error!("Failed to subscribe to gift wrap events: {e}");
}
}
} }
// Get messages with user key // Get messages with user key
@@ -300,11 +303,12 @@ impl DeviceRegistry {
.map(|relay| (relay, vec![filter.clone()])) .map(|relay| (relay, vec![filter.clone()]))
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
if let Err(e) = client.subscribe(target).with_id(id).await { client.subscribe(target).with_id(id).await?;
log::error!("Failed to subscribe to gift wrap events: {e}");
} Ok(())
}) });
.detach();
task.detach();
} }
/// Get device announcement for current user /// Get device announcement for current user
@@ -312,11 +316,9 @@ impl DeviceRegistry {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let task: Task<Result<Event, Error>> = cx.background_spawn(async move { let task: Task<Result<Event, Error>> = cx.background_spawn(async move {
let urls = write_relays.await; let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
// Construct the filter for the device announcement event // Construct the filter for the device announcement event
let filter = Filter::new() let filter = Filter::new()
@@ -324,14 +326,8 @@ impl DeviceRegistry {
.author(public_key) .author(public_key)
.limit(1); .limit(1);
// Construct target for subscription
let target = urls
.iter()
.map(|relay| (relay, vec![filter.clone()]))
.collect::<HashMap<_, _>>();
let mut stream = client let mut stream = client
.stream_events(target) .stream_events(filter)
.timeout(Duration::from_secs(TIMEOUT)) .timeout(Duration::from_secs(TIMEOUT))
.await?; .await?;
@@ -373,16 +369,12 @@ impl DeviceRegistry {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let public_key = nostr.read(cx).identity().read(cx).public_key(); // Generate a new device keys
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let keys = Keys::generate(); let keys = Keys::generate();
let secret = keys.secret_key().to_secret_hex(); let secret = keys.secret_key().to_secret_hex();
let n = keys.public_key(); let n = keys.public_key();
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let urls = write_relays.await;
// Construct an announcement event // Construct an announcement event
let builder = EventBuilder::new(Kind::Custom(10044), "").tags(vec![ let builder = EventBuilder::new(Kind::Custom(10044), "").tags(vec![
Tag::custom(TagKind::custom("n"), vec![n]), Tag::custom(TagKind::custom("n"), vec![n]),
@@ -391,7 +383,7 @@ impl DeviceRegistry {
let event = client.sign_event_builder(builder).await?; let event = client.sign_event_builder(builder).await?;
// Publish announcement // Publish announcement
client.send_event(&event).to(urls).await?; client.send_event(&event).to_nip65().await?;
// Save device keys to the database // Save device keys to the database
Self::set_keys(&client, &secret).await?; Self::set_keys(&client, &secret).await?;
@@ -459,11 +451,9 @@ impl DeviceRegistry {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let urls = write_relays.await; let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
// Construct a filter for device key requests // Construct a filter for device key requests
let filter = Filter::new() let filter = Filter::new()
@@ -471,14 +461,8 @@ impl DeviceRegistry {
.author(public_key) .author(public_key)
.since(Timestamp::now()); .since(Timestamp::now());
// Construct target for subscription
let target = urls
.iter()
.map(|relay| (relay, vec![filter.clone()]))
.collect::<HashMap<_, _>>();
// Subscribe to the device key requests on user's write relays // Subscribe to the device key requests on user's write relays
client.subscribe(target).await?; client.subscribe(filter).await?;
Ok(()) Ok(())
}); });
@@ -491,11 +475,9 @@ impl DeviceRegistry {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let urls = write_relays.await; let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
// Construct a filter for device key requests // Construct a filter for device key requests
let filter = Filter::new() let filter = Filter::new()
@@ -503,14 +485,8 @@ impl DeviceRegistry {
.author(public_key) .author(public_key)
.since(Timestamp::now()); .since(Timestamp::now());
// Construct target for subscription
let target = urls
.iter()
.map(|relay| (relay, vec![filter.clone()]))
.collect::<HashMap<_, _>>();
// Subscribe to the device key requests on user's write relays // Subscribe to the device key requests on user's write relays
client.subscribe(target).await?; client.subscribe(filter).await?;
Ok(()) Ok(())
}); });
@@ -523,9 +499,6 @@ impl DeviceRegistry {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let app_keys = nostr.read(cx).app_keys().clone(); let app_keys = nostr.read(cx).app_keys().clone();
let app_pubkey = app_keys.public_key(); let app_pubkey = app_keys.public_key();
@@ -557,8 +530,6 @@ impl DeviceRegistry {
Ok(Some(keys)) Ok(Some(keys))
} }
None => { None => {
let urls = write_relays.await;
// Construct an event for device key request // Construct an event for device key request
let builder = EventBuilder::new(Kind::Custom(4454), "").tags(vec![ let builder = EventBuilder::new(Kind::Custom(4454), "").tags(vec![
Tag::client(app_name()), Tag::client(app_name()),
@@ -567,7 +538,7 @@ impl DeviceRegistry {
let event = client.sign_event_builder(builder).await?; let event = client.sign_event_builder(builder).await?;
// Send the event to write relays // Send the event to write relays
client.send_event(&event).to(urls).await?; client.send_event(&event).to_nip65().await?;
Ok(None) Ok(None)
} }
@@ -640,11 +611,7 @@ impl DeviceRegistry {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let public_key = nostr.read(cx).identity().read(cx).public_key();
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let urls = write_relays.await;
let signer = client.signer().context("Signer not found")?; let signer = client.signer().context("Signer not found")?;
// Get device keys // Get device keys
@@ -673,7 +640,7 @@ impl DeviceRegistry {
let event = client.sign_event_builder(builder).await?; let event = client.sign_event_builder(builder).await?;
// Send the response event to the user's relay list // Send the response event to the user's relay list
client.send_event(&event).to(urls).await?; client.send_event(&event).to_nip65().await?;
Ok(()) Ok(())
}); });

View File

@@ -138,16 +138,36 @@ impl RelayAuth {
let mut notifications = client.notifications(); let mut notifications = client.notifications();
while let Some(notification) = notifications.next().await { while let Some(notification) = notifications.next().await {
if let ClientNotification::Message { match notification {
message: RelayMessage::Auth { challenge }, ClientNotification::Message { relay_url, message } => {
relay_url, match message {
} = notification RelayMessage::Auth { challenge } => {
{ let request = AuthRequest::new(challenge, relay_url);
let request = AuthRequest::new(challenge, relay_url);
if let Err(e) = tx.send_async(request).await { if let Err(e) = tx.send_async(request).await {
log::error!("Failed to send auth request: {}", e); log::error!("Failed to send auth request: {}", e);
}
}
RelayMessage::Ok {
event_id, message, ..
} => {
let msg = MachineReadablePrefix::parse(&message);
let mut tracker = tracker().write().await;
// Handle authentication messages
if let Some(MachineReadablePrefix::AuthRequired) = msg {
// Keep track of events that need to be resent after authentication
tracker.add_to_pending(event_id, relay_url);
} else {
// Keep track of events sent by Coop
tracker.sent(event_id)
}
}
_ => {}
}
} }
ClientNotification::Shutdown => break,
_ => {}
} }
} }
} }

View File

@@ -1,6 +1,6 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Context as AnyhowContext, Error};
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -121,7 +121,7 @@ pub struct AppSettings {
_subscriptions: SmallVec<[Subscription; 1]>, _subscriptions: SmallVec<[Subscription; 1]>,
/// Background tasks /// Background tasks
_tasks: SmallVec<[Task<()>; 1]>, tasks: SmallVec<[Task<()>; 1]>,
} }
impl AppSettings { impl AppSettings {
@@ -163,8 +163,8 @@ impl AppSettings {
Self { Self {
values: Settings::default(), values: Settings::default(),
tasks,
_subscriptions: subscriptions, _subscriptions: subscriptions,
_tasks: tasks,
} }
} }
@@ -172,7 +172,6 @@ impl AppSettings {
fn get_from_database(cx: &App) -> Task<Result<Settings, Error>> { fn get_from_database(cx: &App) -> Task<Result<Settings, Error>> {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let current_user = nostr.read(cx).identity().read(cx).public_key;
cx.background_spawn(async move { cx.background_spawn(async move {
// Construct a filter to get the latest settings // Construct a filter to get the latest settings
@@ -181,9 +180,12 @@ impl AppSettings {
.identifier(SETTINGS_IDENTIFIER) .identifier(SETTINGS_IDENTIFIER)
.limit(1); .limit(1);
if let Some(public_key) = current_user { // If the signer is available, get settings belonging to the current user
// Push author to the filter if let Some(signer) = client.signer() {
filter = filter.author(public_key); if let Ok(public_key) = signer.get_public_key().await {
// Push author to the filter
filter = filter.author(public_key);
}
} }
if let Some(event) = client.database().query(filter).await?.first_owned() { if let Some(event) = client.database().query(filter).await?.first_owned() {
@@ -198,7 +200,7 @@ impl AppSettings {
pub fn load(&mut self, cx: &mut Context<Self>) { pub fn load(&mut self, cx: &mut Context<Self>) {
let task = Self::get_from_database(cx); let task = Self::get_from_database(cx);
self._tasks.push( self.tasks.push(
// Run task in the background // Run task in the background
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
if let Ok(settings) = task.await { if let Ok(settings) = task.await {
@@ -216,10 +218,12 @@ impl AppSettings {
pub fn save(&mut self, cx: &mut Context<Self>) { pub fn save(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let client = nostr.read(cx).client();
let public_key = nostr.read(cx).identity().read(cx).public_key();
if let Ok(content) = serde_json::to_string(&self.values) { if let Ok(content) = serde_json::to_string(&self.values) {
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
let event = EventBuilder::new(Kind::ApplicationSpecificData, content) let event = EventBuilder::new(Kind::ApplicationSpecificData, content)
.tag(Tag::identifier(SETTINGS_IDENTIFIER)) .tag(Tag::identifier(SETTINGS_IDENTIFIER))
.build(public_key) .build(public_key)

View File

@@ -10,7 +10,7 @@ common = { path = "../common" }
nostr-sdk.workspace = true nostr-sdk.workspace = true
nostr-lmdb.workspace = true nostr-lmdb.workspace = true
nostr-connect.workspace = true nostr-connect.workspace = true
nostr-gossip-memory.workspace = true nostr-gossip-sqlite.workspace = true
gpui.workspace = true gpui.workspace = true
gpui_tokio.workspace = true gpui_tokio.workspace = true

View File

@@ -1,101 +0,0 @@
use nostr_sdk::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RelayState {
#[default]
Initial,
NotSet,
Set,
}
impl RelayState {
pub fn is_initial(&self) -> bool {
matches!(self, RelayState::Initial)
}
}
/// Identity
#[derive(Debug, Default)]
pub struct Identity {
/// Signer's public key
pub public_key: Option<PublicKey>,
/// Whether the identity is owned by the user
pub owned: bool,
/// Status of the current user NIP-65 relays
relay_list: RelayState,
/// Status of the current user NIP-17 relays
messaging_relays: RelayState,
}
impl AsRef<Identity> for Identity {
fn as_ref(&self) -> &Identity {
self
}
}
impl Identity {
pub fn new() -> Self {
Self {
public_key: None,
owned: true,
relay_list: RelayState::default(),
messaging_relays: RelayState::default(),
}
}
/// Resets the relay states to their default values.
pub fn reset_relay_state(&mut self) {
self.relay_list = RelayState::default();
self.messaging_relays = RelayState::default();
}
/// Sets the state of the NIP-65 relays.
pub fn set_relay_list_state(&mut self, state: RelayState) {
self.relay_list = state;
}
/// Returns the state of the NIP-65 relays.
pub fn relay_list_state(&self) -> RelayState {
self.relay_list
}
/// Sets the state of the NIP-17 relays.
pub fn set_messaging_relays_state(&mut self, state: RelayState) {
self.messaging_relays = state;
}
/// Returns the state of the NIP-17 relays.
pub fn messaging_relays_state(&self) -> RelayState {
self.messaging_relays
}
/// Force getting the public key of the identity.
///
/// Panics if the public key is not set.
pub fn public_key(&self) -> PublicKey {
self.public_key.unwrap()
}
/// Returns true if the identity has a public key.
pub fn has_public_key(&self) -> bool {
self.public_key.is_some()
}
/// Sets the public key of the identity.
pub fn set_public_key(&mut self, public_key: PublicKey) {
self.public_key = Some(public_key);
}
/// Unsets the public key of the identity.
pub fn unset_public_key(&mut self) {
self.public_key = None;
}
/// Sets whether the identity is owned by the user.
pub fn set_owned(&mut self, owned: bool) {
self.owned = owned;
}
}

View File

@@ -1,31 +1,29 @@
use std::collections::{HashMap, HashSet}; use std::collections::HashMap;
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Context as AnyhowContext, Error};
use common::{config_dir, CLIENT_NAME}; use common::{config_dir, CLIENT_NAME};
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
use gpui_tokio::Tokio;
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
use nostr_gossip_sqlite::prelude::*;
use nostr_lmdb::NostrLmdb; use nostr_lmdb::NostrLmdb;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
mod device; mod device;
mod event; mod event;
mod gossip; mod gossip;
mod identity;
mod nip05; mod nip05;
mod signer; mod signer;
pub use device::*; pub use device::*;
pub use event::*; pub use event::*;
pub use gossip::*; pub use gossip::*;
pub use identity::*;
pub use nip05::*; pub use nip05::*;
pub use signer::*; pub use signer::*;
use crate::identity::Identity;
/// Default timeout for subscription /// Default timeout for subscription
pub const TIMEOUT: u64 = 3; pub const TIMEOUT: u64 = 3;
/// Default delay for searching /// Default delay for searching
@@ -55,9 +53,19 @@ pub const BOOTSTRAP_RELAYS: [&str; 4] = [
]; ];
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
// rustls uses the `aws_lc_rs` provider by default
// This only errors if the default provider has already
// been installed. We can ignore this `Result`.
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.ok();
// Initialize the tokio runtime // Initialize the tokio runtime
gpui_tokio::init(cx); gpui_tokio::init(cx);
// Initialize the event tracker
let _tracker = tracker();
NostrRegistry::set_global(cx.new(NostrRegistry::new), cx); NostrRegistry::set_global(cx.new(NostrRegistry::new), cx);
} }
@@ -74,23 +82,26 @@ pub struct NostrRegistry {
/// Nostr signer /// Nostr signer
signer: Arc<CoopSigner>, signer: Arc<CoopSigner>,
/// By default, Coop generates a new signer for new users.
///
/// This flag indicates whether the signer is user-owned or Coop-generated.
owned_signer: bool,
/// NIP-65 relay state
nip65: Entity<RelayState>,
/// NIP-17 relay state
nip17: Entity<RelayState>,
/// App keys /// App keys
/// ///
/// Used for Nostr Connect and NIP-4e operations /// Used for Nostr Connect and NIP-4e operations
app_keys: Keys, app_keys: Keys,
/// Current identity (user's public key)
///
/// Set by the current Nostr signer
identity: Entity<Identity>,
/// Gossip implementation
gossip: Entity<Gossip>,
/// Tasks for asynchronous operations /// Tasks for asynchronous operations
tasks: Vec<Task<Result<(), Error>>>, tasks: Vec<Task<Result<(), Error>>>,
/// Subscriptions /// Event subscriptions
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@@ -107,27 +118,39 @@ impl NostrRegistry {
/// Create a new nostr instance /// Create a new nostr instance
fn new(cx: &mut Context<Self>) -> Self { fn new(cx: &mut Context<Self>) -> Self {
// rustls uses the `aws_lc_rs` provider by default // Construct the nostr lmdb instance
// This only errors if the default provider has already
// been installed. We can ignore this `Result`.
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.ok();
// Construct the lmdb
let lmdb = cx.foreground_executor().block_on(async move { let lmdb = cx.foreground_executor().block_on(async move {
NostrLmdb::open(config_dir().join("nostr")) NostrLmdb::open(config_dir().join("nostr"))
.await .await
.expect("Failed to initialize database") .expect("Failed to initialize database")
}); });
// Use tokio to spawn a task to build the gossip instance
let build_gossip_sqlite = Tokio::spawn(cx, async move {
NostrGossipSqlite::open(config_dir().join("gossip"))
.await
.expect("Failed to initialize gossip")
});
// Initialize the nostr gossip instance
let gossip = cx.foreground_executor().block_on(async move {
build_gossip_sqlite
.await
.expect("Failed to initialize gossip")
});
// Construct the nostr signer // Construct the nostr signer
let keys = Keys::generate(); let app_keys = Self::create_or_init_app_keys().unwrap_or(Keys::generate());
let signer = Arc::new(CoopSigner::new(keys)); let signer = Arc::new(CoopSigner::new(app_keys.clone()));
// Construct the relay states entity
let nip65 = cx.new(|_| RelayState::default());
let nip17 = cx.new(|_| RelayState::default());
// Construct the nostr client // Construct the nostr client
let client = ClientBuilder::default() let client = ClientBuilder::default()
.signer(signer.clone()) .signer(signer.clone())
.gossip(gossip)
.database(lmdb) .database(lmdb)
.automatic_authentication(false) .automatic_authentication(false)
.verify_subscriptions(false) .verify_subscriptions(false)
@@ -136,83 +159,23 @@ impl NostrRegistry {
}) })
.build(); .build();
// Construct the event tracker
let _tracker = tracker();
// Get the app keys
let app_keys = Self::create_or_init_app_keys().unwrap();
// Construct the gossip entity
let gossip = cx.new(|_| Gossip::default());
let async_gossip = gossip.downgrade();
// Construct the identity entity
let identity = cx.new(|_| Identity::new());
// Channel for communication between nostr and gpui
let (tx, rx) = flume::bounded::<Event>(2048);
let mut subscriptions = vec![]; let mut subscriptions = vec![];
let mut tasks = vec![];
subscriptions.push( subscriptions.push(
// Observe the identity entity // Observe the NIP-65 state
cx.observe(&identity, |this, state, cx| { cx.observe(&nip65, |this, state, cx| {
if state.read(cx).has_public_key() { if state.read(cx).configured() {
match state.read(cx).relay_list_state() { this.get_profile(cx);
RelayState::Initial => { this.get_messaging_relays(cx);
this.get_relay_list(cx);
}
RelayState::Set => {
if state.read(cx).messaging_relays_state() == RelayState::Initial {
this.get_profile(cx);
this.get_messaging_relays(cx);
};
}
_ => {}
}
} }
}), }),
); );
tasks.push(
// Handle nostr notifications
cx.background_spawn({
let client = client.clone();
async move { Self::handle_notifications(&client, &tx).await }
}),
);
tasks.push(
// Update GPUI states
cx.spawn(async move |_this, cx| {
while let Ok(event) = rx.recv_async().await {
match event.kind {
Kind::RelayList => {
async_gossip.update(cx, |this, cx| {
this.insert_relays(&event);
cx.notify();
})?;
}
Kind::InboxRelays => {
async_gossip.update(cx, |this, cx| {
this.insert_messaging_relays(&event);
cx.notify();
})?;
}
_ => {}
}
}
Ok(())
}),
);
cx.defer(|cx| { cx.defer(|cx| {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
nostr.update(cx, |this, cx| { nostr.update(cx, |this, cx| {
this.connect(cx);
this.get_identity(cx); this.get_identity(cx);
}); });
}); });
@@ -220,89 +183,188 @@ impl NostrRegistry {
Self { Self {
client, client,
signer, signer,
owned_signer: false,
nip65,
nip17,
app_keys, app_keys,
identity, tasks: vec![],
gossip,
_subscriptions: subscriptions, _subscriptions: subscriptions,
tasks,
} }
} }
/// Handle nostr notifications /// Connect to all bootstrap relays
async fn handle_notifications(client: &Client, tx: &flume::Sender<Event>) -> Result<(), Error> { fn connect(&mut self, cx: &mut Context<Self>) {
// Add bootstrap relay to the relay pool let client = self.client();
for url in BOOTSTRAP_RELAYS.into_iter() {
client.add_relay(url).await?;
}
// Add search relay to the relay pool self.tasks.push(cx.background_spawn(async move {
for url in SEARCH_RELAYS.into_iter() { // Add bootstrap relay to the relay pool
client.add_relay(url).await?; for url in BOOTSTRAP_RELAYS.into_iter() {
} client.add_relay(url).await?;
}
// Add wot relay to the relay pool // Add search relay to the relay pool
for url in WOT_RELAYS.into_iter() { for url in SEARCH_RELAYS.into_iter() {
client.add_relay(url).await?; client.add_relay(url).await?;
} }
// Connect to all added relays // Add wot relay to the relay pool
client.connect().await; for url in WOT_RELAYS.into_iter() {
client.add_relay(url).await?;
}
// Handle nostr notifications // Connect to all added relays
let mut notifications = client.notifications(); client.connect().await;
let mut processed_events = HashSet::new();
while let Some(notification) = notifications.next().await { Ok(())
if let ClientNotification::Message { message, relay_url } = notification { }));
match message { }
RelayMessage::Event {
event,
subscription_id,
} => {
if !processed_events.insert(event.id) {
// Skip if the event has already been processed
continue;
}
match event.kind { /// Get the nostr client
Kind::RelayList => { pub fn client(&self) -> Client {
// Automatically get messaging relays for each member when the user opens a room self.client.clone()
if subscription_id.as_str().starts_with("room-") { }
Self::get_adv_events_by(client, event.as_ref()).await?;
}
tx.send_async(event.into_owned()).await?; /// Get the app keys
} pub fn app_keys(&self) -> &Keys {
Kind::InboxRelays => { &self.app_keys
tx.send_async(event.into_owned()).await?; }
}
_ => {}
}
}
RelayMessage::Ok {
event_id, message, ..
} => {
let msg = MachineReadablePrefix::parse(&message);
let mut tracker = tracker().write().await;
// Handle authentication messages /// Returns whether the current signer is owned by user
if let Some(MachineReadablePrefix::AuthRequired) = msg { pub fn owned_signer(&self) -> bool {
// Keep track of events that need to be resent after authentication self.owned_signer
tracker.add_to_pending(event_id, relay_url); }
} else {
// Keep track of events sent by Coop /// Set whether the current signer is owned by user
tracker.sent(event_id) pub fn set_owned_signer(&mut self, owned: bool, cx: &mut Context<Self>) {
} self.owned_signer = owned;
} cx.notify();
_ => {} }
/// Get the NIP-65 state
pub fn nip65_state(&self) -> Entity<RelayState> {
self.nip65.clone()
}
/// Get the NIP-17 state
pub fn nip17_state(&self) -> Entity<RelayState> {
self.nip17.clone()
}
/// Get current signer's public key
pub fn signer_pkey(&self, cx: &App) -> Task<Result<PublicKey, Error>> {
let client = self.client();
cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
Ok(public_key)
})
}
/// Get a relay hint (messaging relay) for a given public key
///
/// Used for building chat messages
pub fn relay_hint(&self, public_key: &PublicKey, cx: &App) -> Task<Option<RelayUrl>> {
let client = self.client();
let public_key = public_key.to_owned();
cx.background_spawn(async move {
let filter = Filter::new()
.author(public_key)
.kind(Kind::InboxRelays)
.limit(1);
if let Ok(events) = client.database().query(filter).await {
if let Some(event) = events.first_owned() {
let relays: Vec<RelayUrl> = nip17::extract_owned_relay_list(event).collect();
return relays.first().cloned();
} }
} }
}
Ok(()) None
})
} }
/// Automatically get messaging relays and encryption announcement from a received relay list /// Get a list of messaging relays with current signer's public key
pub fn messaging_relays(&self, cx: &App) -> Task<Vec<RelayUrl>> {
let client = self.client();
cx.background_spawn(async move {
let Ok(signer) = client.signer().context("Signer not found") else {
return vec![];
};
let Ok(public_key) = signer.get_public_key().await else {
return vec![];
};
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
client
.database()
.query(filter)
.await
.ok()
.and_then(|events| events.first_owned())
.map(|event| nip17::extract_owned_relay_list(event).collect())
.unwrap_or_default()
})
}
/// Reset all relay states
pub fn reset_relay_states(&mut self, cx: &mut Context<Self>) {
self.nip65.update(cx, |this, cx| {
*this = RelayState::default();
cx.notify();
});
self.nip17.update(cx, |this, cx| {
*this = RelayState::default();
cx.notify();
});
}
/// Set the signer for the nostr client and verify the public key
pub fn set_signer<T>(&mut self, new: T, owned: bool, cx: &mut Context<Self>)
where
T: NostrSigner + 'static,
{
let client = self.client();
let signer = self.signer.clone();
// Create a task to update the signer and verify the public key
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
// Update signer
signer.switch(new).await;
// Verify signer
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
log::info!("Signer's public key: {public_key}");
Ok(())
});
self.tasks.push(cx.spawn(async move |this, cx| {
// set signer
task.await?;
// Update states
this.update(cx, |this, cx| {
this.reset_relay_states(cx);
this.get_relay_list(cx);
this.set_owned_signer(owned, cx);
})?;
Ok(())
}));
}
/*
async fn get_adv_events_by(client: &Client, event: &Event) -> Result<(), Error> { async fn get_adv_events_by(client: &Client, event: &Event) -> Result<(), Error> {
// Subscription options // Subscription options
let opts = SubscribeAutoCloseOptions::default() let opts = SubscribeAutoCloseOptions::default()
@@ -348,6 +410,7 @@ impl NostrRegistry {
Ok(()) Ok(())
} }
*/
/// Get or create a new app keys /// Get or create a new app keys
fn create_or_init_app_keys() -> Result<Keys, Error> { fn create_or_init_app_keys() -> Result<Keys, Error> {
@@ -377,124 +440,15 @@ impl NostrRegistry {
Ok(keys) Ok(keys)
} }
/// Get the nostr client
pub fn client(&self) -> Client {
self.client.clone()
}
/// Get the app keys
pub fn app_keys(&self) -> &Keys {
&self.app_keys
}
/// Get current identity
pub fn identity(&self) -> Entity<Identity> {
self.identity.clone()
}
/// Get a relay hint (messaging relay) for a given public key
pub fn relay_hint(&self, public_key: &PublicKey, cx: &App) -> Option<RelayUrl> {
self.gossip
.read(cx)
.messaging_relays(public_key)
.first()
.cloned()
}
/// Get a list of write relays for a given public key
pub fn write_relays(&self, public_key: &PublicKey, cx: &App) -> Task<Vec<RelayUrl>> {
let client = self.client();
let relays = self.gossip.read(cx).write_relays(public_key);
cx.background_spawn(async move {
// Ensure relay connections
for url in relays.iter() {
client.add_relay(url).await.ok();
client.connect_relay(url).await.ok();
}
relays
})
}
/// Get a list of read relays for a given public key
pub fn read_relays(&self, public_key: &PublicKey, cx: &App) -> Task<Vec<RelayUrl>> {
let client = self.client();
let relays = self.gossip.read(cx).read_relays(public_key);
cx.background_spawn(async move {
// Ensure relay connections
for url in relays.iter() {
client.add_relay(url).await.ok();
client.connect_relay(url).await.ok();
}
relays
})
}
/// Get a list of messaging relays for a given public key
pub fn messaging_relays(&self, public_key: &PublicKey, cx: &App) -> Task<Vec<RelayUrl>> {
let client = self.client();
let relays = self.gossip.read(cx).messaging_relays(public_key);
cx.background_spawn(async move {
// Ensure relay connections
for url in relays.iter() {
client.add_relay(url).await.ok();
client.connect_relay(url).await.ok();
}
relays
})
}
/// Set the signer for the nostr client and verify the public key
pub fn set_signer<T>(&mut self, new: T, owned: bool, cx: &mut Context<Self>)
where
T: NostrSigner + 'static,
{
let identity = self.identity.downgrade();
let signer = self.signer.clone();
// Create a task to update the signer and verify the public key
let task: Task<Result<PublicKey, Error>> = cx.background_spawn(async move {
// Update signer
signer.switch(new).await;
// Verify signer
let public_key = signer.get_public_key().await?;
log::info!("test: {public_key:?}");
Ok(public_key)
});
self.tasks.push(cx.spawn(async move |_this, cx| {
match task.await {
Ok(public_key) => {
identity.update(cx, |this, cx| {
this.set_public_key(public_key);
this.reset_relay_state();
this.set_owned(owned);
cx.notify();
})?;
}
Err(e) => {
log::error!("Failed to set signer: {e}");
}
};
Ok(())
}));
}
// Get relay list for current user // Get relay list for current user
fn get_relay_list(&mut self, cx: &mut Context<Self>) { fn get_relay_list(&mut self, cx: &mut Context<Self>) {
let client = self.client(); let client = self.client();
let async_identity = self.identity.downgrade(); let nip65 = self.nip65.downgrade();
let public_key = self.identity().read(cx).public_key();
let task: Task<Result<RelayState, Error>> = cx.background_spawn(async move { let task: Task<Result<RelayState, Error>> = cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
let filter = Filter::new() let filter = Filter::new()
.kind(Kind::RelayList) .kind(Kind::RelayList)
.author(public_key) .author(public_key)
@@ -531,7 +485,7 @@ impl NostrRegistry {
// Subscribe to the relay list events // Subscribe to the relay list events
client.subscribe(target).await?; client.subscribe(target).await?;
return Ok(RelayState::Set); return Ok(RelayState::Configured);
} }
Err(e) => { Err(e) => {
log::error!("Failed to receive relay list event: {e}"); log::error!("Failed to receive relay list event: {e}");
@@ -539,15 +493,15 @@ impl NostrRegistry {
} }
} }
Ok(RelayState::NotSet) Ok(RelayState::NotConfigured)
}); });
self.tasks.push(cx.spawn(async move |_this, cx| { self.tasks.push(cx.spawn(async move |_this, cx| {
match task.await { match task.await {
Ok(state) => { Ok(new_state) => {
async_identity nip65
.update(cx, |this, cx| { .update(cx, |this, cx| {
this.set_relay_list_state(state); *this = new_state;
cx.notify(); cx.notify();
}) })
.ok(); .ok();
@@ -561,19 +515,78 @@ impl NostrRegistry {
})); }));
} }
/// Get messaging relays for current user
fn get_messaging_relays(&mut self, cx: &mut Context<Self>) {
let client = self.client();
let nip17 = self.nip17.downgrade();
let task: Task<Result<RelayState, Error>> = cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
// Construct the filter for inbox relays
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
// Stream events from the write relays
let mut stream = client
.stream_events(filter)
.timeout(Duration::from_secs(TIMEOUT))
.await?;
while let Some((_url, res)) = stream.next().await {
match res {
Ok(event) => {
log::info!("Received messaging relays event: {event:?}");
// Construct a filter to continuously receive relay list events
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.since(Timestamp::now());
// Subscribe to the relay list events
client.subscribe(filter).await?;
return Ok(RelayState::Configured);
}
Err(e) => {
log::error!("Failed to get messaging relays: {e}");
}
}
}
Ok(RelayState::NotConfigured)
});
self.tasks.push(cx.spawn(async move |_this, cx| {
match task.await {
Ok(new_state) => {
nip17
.update(cx, |this, cx| {
*this = new_state;
cx.notify();
})
.ok();
}
Err(e) => {
log::error!("Failed to get messaging relays: {e}");
}
}
Ok(())
}));
}
/// Get profile and contact list for current user /// Get profile and contact list for current user
fn get_profile(&mut self, cx: &mut Context<Self>) { fn get_profile(&mut self, cx: &mut Context<Self>) {
let client = self.client(); let client = self.client();
let public_key = self.identity().read(cx).public_key();
let write_relays = self.write_relays(&public_key, cx);
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let mut urls = write_relays.await; let signer = client.signer().context("Signer not found")?;
urls.extend( let public_key = signer.get_public_key().await?;
BOOTSTRAP_RELAYS
.iter()
.filter_map(|url| RelayUrl::parse(url).ok()),
);
// Construct subscription options // Construct subscription options
let opts = SubscribeAutoCloseOptions::default() let opts = SubscribeAutoCloseOptions::default()
@@ -592,13 +605,10 @@ impl NostrRegistry {
.limit(1) .limit(1)
.author(public_key); .author(public_key);
// Construct targets for subscription client
let target = urls .subscribe(vec![metadata, contact_list])
.into_iter() .close_on(opts)
.map(|relay| (relay, vec![metadata.clone(), contact_list.clone()])) .await?;
.collect::<HashMap<_, _>>();
client.subscribe(target).close_on(opts).await?;
Ok(()) Ok(())
}); });
@@ -606,90 +616,14 @@ impl NostrRegistry {
task.detach(); task.detach();
} }
/// Get messaging relays for current user
fn get_messaging_relays(&mut self, cx: &mut Context<Self>) {
let client = self.client();
let async_identity = self.identity.downgrade();
let public_key = self.identity().read(cx).public_key();
let write_relays = self.write_relays(&public_key, cx);
let task: Task<Result<RelayState, Error>> = cx.background_spawn(async move {
let urls = write_relays.await;
// Construct the filter for inbox relays
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
// Construct targets for subscription
let target = urls
.iter()
.map(|relay| (relay, vec![filter.clone()]))
.collect::<HashMap<_, _>>();
// Stream events from the write relays
let mut stream = client
.stream_events(target)
.timeout(Duration::from_secs(TIMEOUT))
.await?;
while let Some((_url, res)) = stream.next().await {
match res {
Ok(event) => {
log::info!("Received messaging relays event: {event:?}");
// Construct a filter to continuously receive relay list events
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.since(Timestamp::now());
// Construct targets for subscription
let target = urls
.iter()
.map(|relay| (relay, vec![filter.clone()]))
.collect::<HashMap<_, _>>();
// Subscribe to the relay list events
client.subscribe(target).await?;
return Ok(RelayState::Set);
}
Err(e) => {
log::error!("Failed to get messaging relays: {e}");
}
}
}
Ok(RelayState::NotSet)
});
self.tasks.push(cx.spawn(async move |_this, cx| {
match task.await {
Ok(state) => {
async_identity
.update(cx, |this, cx| {
this.set_messaging_relays_state(state);
cx.notify();
})
.ok();
}
Err(e) => {
log::error!("Failed to get messaging relays: {e}");
}
}
Ok(())
}));
}
/// Get contact list for the current user /// Get contact list for the current user
pub fn get_contact_list(&self, cx: &App) -> Task<Result<Vec<PublicKey>, Error>> { pub fn get_contact_list(&self, cx: &App) -> Task<Result<Vec<PublicKey>, Error>> {
let client = self.client(); let client = self.client();
let public_key = self.identity().read(cx).public_key();
cx.background_spawn(async move { cx.background_spawn(async move {
let signer = client.signer().context("Signer not found")?;
let public_key = signer.get_public_key().await?;
let contacts = client.database().contacts_public_keys(public_key).await?; let contacts = client.database().contacts_public_keys(public_key).await?;
let results = contacts.into_iter().collect(); let results = contacts.into_iter().collect();
@@ -700,19 +634,15 @@ impl NostrRegistry {
/// Set the metadata for the current user /// Set the metadata for the current user
pub fn set_metadata(&self, metadata: &Metadata, cx: &App) -> Task<Result<(), Error>> { pub fn set_metadata(&self, metadata: &Metadata, cx: &App) -> Task<Result<(), Error>> {
let client = self.client(); let client = self.client();
let public_key = self.identity().read(cx).public_key();
let write_relays = self.write_relays(&public_key, cx);
let metadata = metadata.clone(); let metadata = metadata.clone();
cx.background_spawn(async move { cx.background_spawn(async move {
let urls = write_relays.await;
// Build and sign the metadata event // Build and sign the metadata event
let builder = EventBuilder::metadata(&metadata); let builder = EventBuilder::metadata(&metadata);
let event = client.sign_event_builder(builder).await?; let event = client.sign_event_builder(builder).await?;
// Send event to user's write relayss // Send event to user's write relayss
client.send_event(&event).to(urls).await?; client.send_event(&event).to_nip65().await?;
Ok(()) Ok(())
}) })
@@ -1007,6 +937,29 @@ impl NostrRegistry {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RelayState {
#[default]
Idle,
Checking,
NotConfigured,
Configured,
}
impl RelayState {
pub fn idle(&self) -> bool {
matches!(self, RelayState::Idle)
}
pub fn not_configured(&self) -> bool {
matches!(self, RelayState::NotConfigured)
}
pub fn configured(&self) -> bool {
matches!(self, RelayState::Configured)
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CoopAuthUrlHandler; pub struct CoopAuthUrlHandler;

View File

@@ -1,4 +1,5 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::result::Result;
use std::sync::Arc; use std::sync::Arc;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
@@ -19,10 +20,12 @@ impl CoopSigner {
} }
} }
async fn get(&self) -> Arc<dyn NostrSigner> { /// Get the current signer.
pub async fn get(&self) -> Arc<dyn NostrSigner> {
self.signer.read().await.clone() self.signer.read().await.clone()
} }
/// Switch the current signer to a new signer.
pub async fn switch<T>(&self, new: T) pub async fn switch<T>(&self, new: T)
where where
T: IntoNostrSigner, T: IntoNostrSigner,
@@ -33,40 +36,40 @@ impl CoopSigner {
} }
impl NostrSigner for CoopSigner { impl NostrSigner for CoopSigner {
#[allow(mismatched_lifetime_syntaxes)]
fn backend(&self) -> SignerBackend { fn backend(&self) -> SignerBackend {
SignerBackend::Custom(Cow::Borrowed("custom")) SignerBackend::Custom(Cow::Borrowed("custom"))
} }
fn get_public_key(&self) -> BoxedFuture<Result<PublicKey, SignerError>> { fn get_public_key<'a>(&'a self) -> BoxedFuture<'a, Result<PublicKey, SignerError>> {
Box::pin(async move { Ok(self.get().await.get_public_key().await?) }) Box::pin(async move { self.get().await.get_public_key().await })
} }
fn sign_event( fn sign_event<'a>(
&self, &'a self,
unsigned: UnsignedEvent, unsigned: UnsignedEvent,
) -> BoxedFuture<std::result::Result<Event, SignerError>> { ) -> BoxedFuture<'a, Result<Event, SignerError>> {
Box::pin(async move { Ok(self.get().await.sign_event(unsigned).await?) }) Box::pin(async move { self.get().await.sign_event(unsigned).await })
} }
fn nip04_encrypt<'a>( fn nip04_encrypt<'a>(
&'a self, &'a self,
public_key: &'a PublicKey, public_key: &'a PublicKey,
content: &'a str, content: &'a str,
) -> BoxedFuture<'a, std::result::Result<String, SignerError>> { ) -> BoxedFuture<'a, Result<String, SignerError>> {
Box::pin(async move { Ok(self.get().await.nip04_encrypt(public_key, content).await?) }) Box::pin(async move { self.get().await.nip04_encrypt(public_key, content).await })
} }
fn nip04_decrypt<'a>( fn nip04_decrypt<'a>(
&'a self, &'a self,
public_key: &'a PublicKey, public_key: &'a PublicKey,
encrypted_content: &'a str, encrypted_content: &'a str,
) -> BoxedFuture<'a, std::result::Result<String, SignerError>> { ) -> BoxedFuture<'a, Result<String, SignerError>> {
Box::pin(async move { Box::pin(async move {
Ok(self self.get()
.get()
.await .await
.nip04_decrypt(public_key, encrypted_content) .nip04_decrypt(public_key, encrypted_content)
.await?) .await
}) })
} }
@@ -74,15 +77,15 @@ impl NostrSigner for CoopSigner {
&'a self, &'a self,
public_key: &'a PublicKey, public_key: &'a PublicKey,
content: &'a str, content: &'a str,
) -> BoxedFuture<'a, std::result::Result<String, SignerError>> { ) -> BoxedFuture<'a, Result<String, SignerError>> {
Box::pin(async move { Ok(self.get().await.nip44_encrypt(public_key, content).await?) }) Box::pin(async move { self.get().await.nip44_encrypt(public_key, content).await })
} }
fn nip44_decrypt<'a>( fn nip44_decrypt<'a>(
&'a self, &'a self,
public_key: &'a PublicKey, public_key: &'a PublicKey,
payload: &'a str, payload: &'a str,
) -> BoxedFuture<'a, std::result::Result<String, SignerError>> { ) -> BoxedFuture<'a, Result<String, SignerError>> {
Box::pin(async move { Ok(self.get().await.nip44_decrypt(public_key, payload).await?) }) Box::pin(async move { self.get().await.nip44_decrypt(public_key, payload).await })
} }
} }