refactor chats (#15)

* refactor

* update

* update

* update

* remove nostrprofile struct

* update

* refactor contacts

* prevent double login
This commit is contained in:
reya
2025-04-10 08:10:53 +07:00
committed by GitHub
parent f7610cc9c9
commit 3246abace1
27 changed files with 1166 additions and 909 deletions

509
Cargo.lock generated
View File

@@ -97,12 +97,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.18" version = "0.6.18"
@@ -750,9 +744,9 @@ dependencies = [
[[package]] [[package]]
name = "bstr" name = "bstr"
version = "1.11.3" version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
dependencies = [ dependencies = [
"memchr", "memchr",
"serde", "serde",
@@ -834,12 +828,6 @@ dependencies = [
"wayland-client", "wayland-client",
] ]
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]] [[package]]
name = "cbc" name = "cbc"
version = "0.1.2" version = "0.1.2"
@@ -958,6 +946,7 @@ dependencies = [
name = "chats" name = "chats"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"account",
"anyhow", "anyhow",
"chrono", "chrono",
"common", "common",
@@ -987,33 +976,6 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]] [[package]]
name = "cipher" name = "cipher"
version = "0.4.4" version = "0.4.4"
@@ -1158,7 +1120,7 @@ dependencies = [
[[package]] [[package]]
name = "collections" name = "collections"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
@@ -1317,6 +1279,19 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "core-graphics-helmer-fork"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5"
dependencies = [
"bitflags 2.9.0",
"core-foundation 0.9.4",
"core-graphics-types 0.1.3",
"foreign-types",
"libc",
]
[[package]] [[package]]
name = "core-graphics-types" name = "core-graphics-types"
version = "0.1.3" version = "0.1.3"
@@ -1428,42 +1403,6 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "criterion"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"is-terminal",
"itertools 0.10.5",
"num-traits",
"once_cell",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
"itertools 0.10.5",
]
[[package]] [[package]]
name = "crossbeam-deque" name = "crossbeam-deque"
version = "0.8.6" version = "0.8.6"
@@ -1531,6 +1470,16 @@ version = "0.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f211af61d8efdd104f96e57adf5e426ba1bc3ed7a4ead616e15e5881fd79c4d" checksum = "4f211af61d8efdd104f96e57adf5e426ba1bc3ed7a4ead616e15e5881fd79c4d"
[[package]]
name = "ctrlc"
version = "3.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "697b5419f348fd5ae2478e8018cb016c00a5881c7f46c717de98ffd135a5651c"
dependencies = [
"nix",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "data-encoding" name = "data-encoding"
version = "2.8.0" version = "2.8.0"
@@ -1559,7 +1508,7 @@ dependencies = [
[[package]] [[package]]
name = "derive_refineable" name = "derive_refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1619,6 +1568,12 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "dispatch"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.5"
@@ -2320,7 +2275,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui" name = "gpui"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"as-raw-xcb-connection", "as-raw-xcb-connection",
@@ -2375,6 +2330,7 @@ dependencies = [
"raw-window-handle", "raw-window-handle",
"refineable", "refineable",
"resvg", "resvg",
"scap",
"schemars", "schemars",
"seahash", "seahash",
"semantic_version", "semantic_version",
@@ -2397,8 +2353,8 @@ dependencies = [
"wayland-cursor", "wayland-cursor",
"wayland-protocols", "wayland-protocols",
"wayland-protocols-plasma", "wayland-protocols-plasma",
"windows", "windows 0.61.1",
"windows-core", "windows-core 0.61.0",
"windows-numerics", "windows-numerics",
"workspace-hack", "workspace-hack",
"x11-clipboard", "x11-clipboard",
@@ -2410,7 +2366,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_macros" name = "gpui_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2445,9 +2401,9 @@ dependencies = [
[[package]] [[package]]
name = "half" name = "half"
version = "2.5.0" version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7db2ff139bba50379da6aa0766b52fdcb62cb5b263009b09ed58ba604e14bbd1" checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"crunchy", "crunchy",
@@ -2523,12 +2479,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
[[package]]
name = "hermit-abi"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e"
[[package]] [[package]]
name = "hex" name = "hex"
version = "0.4.3" version = "0.4.3"
@@ -2640,7 +2590,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client" name = "http_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -2657,7 +2607,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client_tls" name = "http_client_tls"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881"
dependencies = [ dependencies = [
"rustls", "rustls",
"rustls-platform-verifier", "rustls-platform-verifier",
@@ -2741,7 +2691,7 @@ dependencies = [
"js-sys", "js-sys",
"log", "log",
"wasm-bindgen", "wasm-bindgen",
"windows-core", "windows-core 0.61.0",
] ]
[[package]] [[package]]
@@ -3018,17 +2968,6 @@ dependencies = [
"once_cell", "once_cell",
] ]
[[package]]
name = "is-terminal"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi 0.5.0",
"libc",
"windows-sys 0.59.0",
]
[[package]] [[package]]
name = "is-wsl" name = "is-wsl"
version = "0.4.0" version = "0.4.0"
@@ -3045,15 +2984,6 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.12.1" version = "0.12.1"
@@ -3402,7 +3332,7 @@ dependencies = [
[[package]] [[package]]
name = "media" name = "media"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bindgen 0.71.1", "bindgen 0.71.1",
@@ -3478,9 +3408,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.7" version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
dependencies = [ dependencies = [
"adler2", "adler2",
"simd-adler32", "simd-adler32",
@@ -3583,7 +3513,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]] [[package]]
name = "nostr" name = "nostr"
version = "0.40.0" version = "0.40.0"
source = "git+https://github.com/rust-nostr/nostr#1347878e90392db54a263d60021fa0410a3b05cb" source = "git+https://github.com/rust-nostr/nostr#f4f3f50aa637b3d288a6f1cf762260005b7b498a"
dependencies = [ dependencies = [
"aes", "aes",
"base64", "base64",
@@ -3608,7 +3538,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-connect" name = "nostr-connect"
version = "0.40.0" version = "0.40.0"
source = "git+https://github.com/rust-nostr/nostr#1347878e90392db54a263d60021fa0410a3b05cb" source = "git+https://github.com/rust-nostr/nostr#f4f3f50aa637b3d288a6f1cf762260005b7b498a"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -3620,7 +3550,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-database" name = "nostr-database"
version = "0.40.0" version = "0.40.0"
source = "git+https://github.com/rust-nostr/nostr#1347878e90392db54a263d60021fa0410a3b05cb" source = "git+https://github.com/rust-nostr/nostr#f4f3f50aa637b3d288a6f1cf762260005b7b498a"
dependencies = [ dependencies = [
"flatbuffers", "flatbuffers",
"lru", "lru",
@@ -3631,7 +3561,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-lmdb" name = "nostr-lmdb"
version = "0.40.0" version = "0.40.0"
source = "git+https://github.com/rust-nostr/nostr#1347878e90392db54a263d60021fa0410a3b05cb" source = "git+https://github.com/rust-nostr/nostr#f4f3f50aa637b3d288a6f1cf762260005b7b498a"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"heed", "heed",
@@ -3644,7 +3574,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-relay-pool" name = "nostr-relay-pool"
version = "0.40.0" version = "0.40.0"
source = "git+https://github.com/rust-nostr/nostr#1347878e90392db54a263d60021fa0410a3b05cb" source = "git+https://github.com/rust-nostr/nostr#f4f3f50aa637b3d288a6f1cf762260005b7b498a"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"async-wsocket", "async-wsocket",
@@ -3661,7 +3591,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-sdk" name = "nostr-sdk"
version = "0.40.0" version = "0.40.0"
source = "git+https://github.com/rust-nostr/nostr#1347878e90392db54a263d60021fa0410a3b05cb" source = "git+https://github.com/rust-nostr/nostr#f4f3f50aa637b3d288a6f1cf762260005b7b498a"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -3672,6 +3602,15 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "ntapi"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.46.0" version = "0.46.0"
@@ -3802,6 +3741,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [ dependencies = [
"malloc_buf", "malloc_buf",
"objc_exception",
]
[[package]]
name = "objc-foundation"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
dependencies = [
"block",
"objc",
"objc_id",
] ]
[[package]] [[package]]
@@ -4006,6 +3957,24 @@ dependencies = [
"objc2-foundation", "objc2-foundation",
] ]
[[package]]
name = "objc_exception"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4"
dependencies = [
"cc",
]
[[package]]
name = "objc_id"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
dependencies = [
"objc",
]
[[package]] [[package]]
name = "object" name = "object"
version = "0.36.7" version = "0.36.7"
@@ -4062,12 +4031,6 @@ dependencies = [
"zvariant", "zvariant",
] ]
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]] [[package]]
name = "opaque-debug" name = "opaque-debug"
version = "0.3.1" version = "0.3.1"
@@ -4307,34 +4270,6 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "plotters"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
[[package]]
name = "plotters-svg"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
dependencies = [
"plotters-backend",
]
[[package]] [[package]]
name = "png" name = "png"
version = "0.17.16" version = "0.17.16"
@@ -4485,6 +4420,15 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.37.4" version = "0.37.4"
@@ -4746,9 +4690,9 @@ dependencies = [
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.10" version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3"
dependencies = [ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
] ]
@@ -4767,7 +4711,7 @@ dependencies = [
[[package]] [[package]]
name = "refineable" name = "refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881"
dependencies = [ dependencies = [
"derive_refineable", "derive_refineable",
"workspace-hack", "workspace-hack",
@@ -4897,7 +4841,7 @@ dependencies = [
[[package]] [[package]]
name = "reqwest_client" name = "reqwest_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -5194,6 +5138,27 @@ dependencies = [
"winapi-util", "winapi-util",
] ]
[[package]]
name = "scap"
version = "0.0.8"
source = "git+https://github.com/zed-industries/scap?rev=08f0a01417505cc0990b9931a37e5120db92e0d0#08f0a01417505cc0990b9931a37e5120db92e0d0"
dependencies = [
"anyhow",
"cocoa 0.25.0",
"core-graphics-helmer-fork",
"log",
"objc",
"rand 0.8.5",
"screencapturekit",
"screencapturekit-sys",
"sysinfo",
"tao-core-video-sys",
"windows 0.61.1",
"windows-capture",
"x11",
"xcb",
]
[[package]] [[package]]
name = "schannel" name = "schannel"
version = "0.1.27" version = "0.1.27"
@@ -5240,6 +5205,29 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "screencapturekit"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a5eeeb57ac94960cfe5ff4c402be6585ae4c8d29a2cf41b276048c2e849d64e"
dependencies = [
"screencapturekit-sys",
]
[[package]]
name = "screencapturekit-sys"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22411b57f7d49e7fe08025198813ee6fd65e1ee5eff4ebc7880c12c82bde4c60"
dependencies = [
"block",
"dispatch",
"objc",
"objc-foundation",
"objc_id",
"once_cell",
]
[[package]] [[package]]
name = "scrypt" name = "scrypt"
version = "0.11.0" version = "0.11.0"
@@ -5310,7 +5298,7 @@ checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe"
[[package]] [[package]]
name = "semantic_version" name = "semantic_version"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"serde", "serde",
@@ -5633,7 +5621,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "sum_tree" name = "sum_tree"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"log", "log",
@@ -5806,6 +5794,20 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "sysinfo"
version = "0.31.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be"
dependencies = [
"core-foundation-sys",
"libc",
"memchr",
"ntapi",
"rayon",
"windows 0.57.0",
]
[[package]] [[package]]
name = "system-configuration" name = "system-configuration"
version = "0.6.1" version = "0.6.1"
@@ -5859,6 +5861,18 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb" checksum = "8bdb6fa0dfa67b38c1e66b7041ba9dcf23b99d8121907cd31c807a332f7a0bbb"
[[package]]
name = "tao-core-video-sys"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6"
dependencies = [
"cfg-if",
"core-foundation-sys",
"libc",
"objc",
]
[[package]] [[package]]
name = "target-lexicon" name = "target-lexicon"
version = "0.12.16" version = "0.12.16"
@@ -6004,16 +6018,6 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.9.0" version = "1.9.0"
@@ -6042,9 +6046,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.44.1" version = "1.44.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",
@@ -6312,7 +6316,6 @@ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"common", "common",
"criterion",
"gpui", "gpui",
"image", "image",
"itertools 0.13.0", "itertools 0.13.0",
@@ -6510,7 +6513,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "util" name = "util"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#c1259c136e41b5870d15271190198ff0075ba35c" source = "git+https://github.com/zed-industries/zed#f0b7f355a29486566a8ed7c2be4acad4b305b881"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-fs", "async-fs",
@@ -6829,7 +6832,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quick-xml", "quick-xml 0.37.4",
"quote", "quote",
] ]
@@ -6943,6 +6946,26 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
dependencies = [
"windows-core 0.57.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"windows-core 0.58.0",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows" name = "windows"
version = "0.61.1" version = "0.61.1"
@@ -6950,19 +6973,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
dependencies = [ dependencies = [
"windows-collections", "windows-collections",
"windows-core", "windows-core 0.61.0",
"windows-future", "windows-future",
"windows-link", "windows-link",
"windows-numerics", "windows-numerics",
] ]
[[package]]
name = "windows-capture"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6001b777f61cafce437201de46a019ed7f4afed3b669f02e5ce4e0759164cb3e"
dependencies = [
"clap",
"ctrlc",
"parking_lot",
"rayon",
"thiserror 1.0.69",
"windows 0.58.0",
]
[[package]] [[package]]
name = "windows-collections" name = "windows-collections"
version = "0.2.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [ dependencies = [
"windows-core", "windows-core 0.61.0",
]
[[package]]
name = "windows-core"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
dependencies = [
"windows-implement 0.57.0",
"windows-interface 0.57.0",
"windows-result 0.1.2",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-implement 0.58.0",
"windows-interface 0.58.0",
"windows-result 0.2.0",
"windows-strings 0.1.0",
"windows-targets 0.52.6",
] ]
[[package]] [[package]]
@@ -6971,8 +7033,8 @@ version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [ dependencies = [
"windows-implement", "windows-implement 0.60.0",
"windows-interface", "windows-interface 0.59.1",
"windows-link", "windows-link",
"windows-result 0.3.2", "windows-result 0.3.2",
"windows-strings 0.4.0", "windows-strings 0.4.0",
@@ -6984,10 +7046,32 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
dependencies = [ dependencies = [
"windows-core", "windows-core 0.61.0",
"windows-link", "windows-link",
] ]
[[package]]
name = "windows-implement"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "windows-implement"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]] [[package]]
name = "windows-implement" name = "windows-implement"
version = "0.60.0" version = "0.60.0"
@@ -6999,6 +7083,28 @@ dependencies = [
"syn 2.0.100", "syn 2.0.100",
] ]
[[package]]
name = "windows-interface"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "windows-interface"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]] [[package]]
name = "windows-interface" name = "windows-interface"
version = "0.59.1" version = "0.59.1"
@@ -7022,7 +7128,7 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [ dependencies = [
"windows-core", "windows-core 0.61.0",
"windows-link", "windows-link",
] ]
@@ -7048,6 +7154,15 @@ dependencies = [
"windows-targets 0.53.0", "windows-targets 0.53.0",
] ]
[[package]]
name = "windows-result"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
dependencies = [
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.2.0" version = "0.2.0"
@@ -7374,9 +7489,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.4" version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" checksum = "63d3fcd9bba44b03821e7d699eeee959f3126dcc4aa8e4ae18ec617c2a5cea10"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -7427,6 +7542,16 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "x11"
version = "2.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e"
dependencies = [
"libc",
"pkg-config",
]
[[package]] [[package]]
name = "x11-clipboard" name = "x11-clipboard"
version = "0.9.3" version = "0.9.3"
@@ -7456,6 +7581,18 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d"
[[package]]
name = "xcb"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1e2f212bb1a92cd8caac8051b829a6582ede155ccb60b5d5908b81b100952be"
dependencies = [
"bitflags 1.3.2",
"libc",
"quick-xml 0.30.0",
"x11",
]
[[package]] [[package]]
name = "xcursor" name = "xcursor"
version = "0.3.8" version = "0.3.8"

View File

@@ -18,6 +18,7 @@ reqwest_client = { git = "https://github.com/zed-industries/zed" }
nostr = { git = "https://github.com/rust-nostr/nostr", features = ["parser"] } nostr = { git = "https://github.com/rust-nostr/nostr", features = ["parser"] }
nostr-relay-builder = { git = "https://github.com/rust-nostr/nostr" } nostr-relay-builder = { 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-keyring = { git = "https://github.com/rust-nostr/nostr" }
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [ nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [
"lmdb", "lmdb",
"nip96", "nip96",

View File

@@ -1,7 +1,6 @@
use std::time::Duration; use std::time::Duration;
use anyhow::Error; use anyhow::Error;
use common::profile::NostrProfile;
use global::{ use global::{
constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID}, constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID},
get_client, get_client,
@@ -15,12 +14,19 @@ struct GlobalAccount(Entity<Account>);
impl Global for GlobalAccount {} impl Global for GlobalAccount {}
pub fn init(cx: &mut App) { pub fn init(cx: &mut App) {
Account::set_global(cx.new(|_| Account { profile: None }), cx); Account::set_global(
cx.new(|_| Account {
profile: None,
loading: false,
}),
cx,
);
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Account { pub struct Account {
pub profile: Option<NostrProfile>, pub profile: Option<Profile>,
loading: bool,
} }
impl Account { impl Account {
@@ -36,7 +42,13 @@ impl Account {
where where
S: NostrSigner + 'static, S: NostrSigner + 'static,
{ {
let task: Task<Result<NostrProfile, Error>> = cx.background_spawn(async move { if self.loading {
return;
}
self.set_loading(true, cx);
let task: Task<Result<Profile, Error>> = cx.background_spawn(async move {
let client = get_client(); let client = get_client();
// Use user's signer for main signer // Use user's signer for main signer
_ = client.set_signer(signer).await; _ = client.set_signer(signer).await;
@@ -44,6 +56,7 @@ impl Account {
// Verify nostr signer and get public key // Verify nostr signer and get public key
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
log::info!("Logged in with public key: {:?}", public_key);
// Fetch user's metadata // Fetch user's metadata
let metadata = client let metadata = client
@@ -51,7 +64,7 @@ impl Account {
.await? .await?
.unwrap_or_default(); .unwrap_or_default();
Ok(NostrProfile::new(public_key, metadata)) Ok(Profile::new(public_key, metadata))
}); });
cx.spawn_in(window, async move |this, cx| match task.await { cx.spawn_in(window, async move |this, cx| match task.await {
@@ -59,6 +72,7 @@ impl Account {
cx.update(|_, cx| { cx.update(|_, cx| {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.profile = Some(profile); this.profile = Some(profile);
this.set_loading(false, cx);
this.subscribe(cx); this.subscribe(cx);
cx.notify(); cx.notify();
}) })
@@ -79,14 +93,14 @@ impl Account {
let client = get_client(); let client = get_client();
let keys = Keys::generate(); let keys = Keys::generate();
let task: Task<Result<NostrProfile, Error>> = cx.background_spawn(async move { let task: Task<Result<Profile, Error>> = cx.background_spawn(async move {
let public_key = keys.public_key(); let public_key = keys.public_key();
// Update signer // Update signer
client.set_signer(keys).await; client.set_signer(keys).await;
// Set metadata // Set metadata
client.set_metadata(&metadata).await?; client.set_metadata(&metadata).await?;
Ok(NostrProfile::new(public_key, metadata)) Ok(Profile::new(public_key, metadata))
}); });
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
@@ -115,7 +129,7 @@ impl Account {
}; };
let client = get_client(); let client = get_client();
let user = profile.public_key; let user = profile.public_key();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let metadata = Filter::new() let metadata = Filter::new()
@@ -164,4 +178,9 @@ impl Account {
}) })
.detach(); .detach();
} }
fn set_loading(&mut self, loading: bool, cx: &mut Context<Self>) {
self.loading = loading;
cx.notify();
}
} }

View File

@@ -1,4 +1,5 @@
use account::Account; use account::Account;
use common::profile::SharedProfile;
use global::get_client; use global::get_client;
use gpui::{ use gpui::{
actions, div, img, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis, actions, div, img, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis,
@@ -172,7 +173,7 @@ impl ChatSpace {
.icon(Icon::new(IconName::ChevronDownSmall)) .icon(Icon::new(IconName::ChevronDownSmall))
.when_some( .when_some(
Account::global(cx).read(cx).profile.as_ref(), Account::global(cx).read(cx).profile.as_ref(),
|this, profile| this.child(img(profile.avatar.clone()).size_5()), |this, profile| this.child(img(profile.shared_avatar()).size_5()),
) )
.popup_menu(move |this, _, _cx| { .popup_menu(move |this, _, _cx| {
this.menu( this.menu(

View File

@@ -18,7 +18,8 @@ use gpui::{point, SharedString, TitlebarOptions};
use gpui::{WindowBackgroundAppearance, WindowDecorations}; use gpui::{WindowBackgroundAppearance, WindowDecorations};
use nostr_sdk::{ use nostr_sdk::{
pool::prelude::ReqExitPolicy, Event, EventBuilder, EventId, Filter, JsonUtil, Keys, Kind, pool::prelude::ReqExitPolicy, Event, EventBuilder, EventId, Filter, JsonUtil, Keys, Kind,
PublicKey, RelayMessage, RelayPoolNotification, SubscribeAutoCloseOptions, SubscriptionId, Tag, Metadata, PublicKey, RelayMessage, RelayPoolNotification, SubscribeAutoCloseOptions,
SubscriptionId, Tag,
}; };
use smol::Timer; use smol::Timer;
use std::{collections::HashSet, mem, sync::Arc, time::Duration}; use std::{collections::HashSet, mem, sync::Arc, time::Duration};
@@ -34,6 +35,8 @@ actions!(coop, [Quit]);
enum Signal { enum Signal {
/// Receive event /// Receive event
Event(Event), Event(Event),
/// Receive metadata
Metadata(Box<(PublicKey, Option<Metadata>)>),
/// Receive EOSE /// Receive EOSE
Eose, Eose,
} }
@@ -149,6 +152,14 @@ fn main() {
event_tx.send(Signal::Event(event)).await.ok(); event_tx.send(Signal::Event(event)).await.ok();
} }
} }
Kind::Metadata => {
let metadata = Metadata::from_json(&event.content).ok();
event_tx
.send(Signal::Metadata(Box::new((event.pubkey, metadata))))
.await
.ok();
}
Kind::ContactList => { Kind::ContactList => {
if let Ok(signer) = client.signer().await { if let Ok(signer) = client.signer().await {
if let Ok(public_key) = signer.get_public_key().await { if let Ok(public_key) = signer.get_public_key().await {
@@ -241,14 +252,23 @@ fn main() {
while let Ok(signal) = event_rx.recv().await { while let Ok(signal) = event_rx.recv().await {
cx.update(|window, cx| { cx.update(|window, cx| {
match signal { match signal {
Signal::Eose => {
chats.update(cx, |this, cx| this.load_rooms(window, cx));
}
Signal::Event(event) => { Signal::Event(event) => {
chats.update(cx, |this, cx| { chats.update(cx, |this, cx| {
this.push_message(event, window, cx) this.push_message(event, window, cx)
}); });
} }
Signal::Metadata(data) => {
chats.update(cx, |this, cx| {
this.add_profile(data.0, data.1, cx)
});
}
Signal::Eose => {
chats.update(cx, |this, cx| {
// This function maybe called multiple times
// TODO: only handle the last EOSE signal
this.load_rooms(window, cx)
});
}
}; };
}) })
.ok(); .ok();

View File

@@ -1,7 +1,7 @@
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use async_utility::task::spawn; use async_utility::task::spawn;
use chats::{message::RoomMessage, room::Room, ChatRegistry}; use chats::{message::RoomMessage, room::Room, ChatRegistry};
use common::utils::nip96_upload; use common::{nip96_upload, profile::SharedProfile};
use global::{constants::IMAGE_SERVICE, get_client}; use global::{constants::IMAGE_SERVICE, get_client};
use gpui::{ use gpui::{
div, img, list, prelude::FluentBuilder, px, relative, svg, white, AnyElement, App, AppContext, div, img, list, prelude::FluentBuilder, px, relative, svg, white, AnyElement, App, AppContext,
@@ -27,7 +27,7 @@ use ui::{
const ALERT: &str = "has not set up Messaging (DM) Relays, so they will NOT receive your messages."; const ALERT: &str = "has not set up Messaging (DM) Relays, so they will NOT receive your messages.";
pub fn init(id: &u64, window: &mut Window, cx: &mut App) -> Result<Arc<Entity<Chat>>, Error> { pub fn init(id: &u64, window: &mut Window, cx: &mut App) -> Result<Arc<Entity<Chat>>, Error> {
if let Some(room) = ChatRegistry::global(cx).read(cx).get(id, cx) { if let Some(room) = ChatRegistry::global(cx).read(cx).room(id, cx) {
Ok(Arc::new(Chat::new(id, room, window, cx))) Ok(Arc::new(Chat::new(id, room, window, cx)))
} else { } else {
Err(anyhow!("Chat Room not found.")) Err(anyhow!("Chat Room not found."))
@@ -137,15 +137,15 @@ impl Chat {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
result.into_iter().for_each(|item| { result.into_iter().for_each(|item| {
if !item.1 { if !item.1 {
if let Some(profile) = let profile = this
this.room.read_with(cx, |this, _| this.member(&item.0)) .room
{ .read_with(cx, |this, _| this.profile_by_pubkey(&item.0, cx));
this.push_system_message( this.push_system_message(
format!("{} {}", profile.name, ALERT), format!("{} {}", profile.shared_name(), ALERT),
cx, cx,
); );
} }
}
}); });
}) })
.ok(); .ok();
@@ -367,7 +367,7 @@ impl Chat {
this.bg(cx.theme().accent.step(cx, ColorScaleStep::NINE)) this.bg(cx.theme().accent.step(cx, ColorScaleStep::NINE))
}), }),
) )
.child(img(item.author.avatar.clone()).size_8().flex_shrink_0()) .child(img(item.author.shared_avatar()).size_8().flex_shrink_0())
.child( .child(
div() div()
.flex() .flex()
@@ -381,17 +381,11 @@ impl Chat {
.gap_2() .gap_2()
.text_xs() .text_xs()
.child( .child(
div().font_semibold().child(item.author.name.clone()), div().font_semibold().child(item.author.shared_name()),
) )
.child( .child(div().child(item.ago()).text_color(
div() cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
.child(item.created_at.human_readable()) )),
.text_color(
cx.theme()
.base
.step(cx, ColorScaleStep::ELEVEN),
),
),
) )
.child(div().text_sm().child(text.element( .child(div().text_sm().child(text.element(
"body".into(), "body".into(),
@@ -445,11 +439,7 @@ impl Panel for Chat {
fn title(&self, cx: &App) -> AnyElement { fn title(&self, cx: &App) -> AnyElement {
self.room.read_with(cx, |this, _| { self.room.read_with(cx, |this, _| {
let facepill: Vec<SharedString> = this let facepill: Vec<SharedString> = this.avatars(cx);
.members
.iter()
.map(|member| member.avatar.clone())
.collect();
div() div()
.flex() .flex()
@@ -461,13 +451,19 @@ impl Panel for Chat {
.flex_row_reverse() .flex_row_reverse()
.items_center() .items_center()
.justify_start() .justify_start()
.children(facepill.into_iter().enumerate().rev().map(|(ix, face)| { .children(
facepill
.into_iter()
.enumerate()
.rev()
.map(|(ix, facepill)| {
div() div()
.when(ix > 0, |div| div.ml_neg_1()) .when(ix > 0, |div| div.ml_neg_1())
.child(img(face).size_4()) .child(img(facepill).size_4())
})), }),
),
) )
.when_some(this.subject(), |this, name| this.child(name)) .child(this.display_name(cx))
.into_any() .into_any()
}) })
} }

View File

@@ -1,10 +1,14 @@
use common::profile::NostrProfile; use std::collections::BTreeSet;
use anyhow::Error;
use common::profile::SharedProfile;
use global::get_client; use global::get_client;
use gpui::{ use gpui::{
div, img, prelude::FluentBuilder, px, uniform_list, AnyElement, App, AppContext, Context, div, img, prelude::FluentBuilder, px, uniform_list, AnyElement, App, AppContext, Context,
Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement,
Render, SharedString, Styled, Window, Render, SharedString, Styled, Task, Window,
}; };
use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use ui::{ use ui::{
button::Button, button::Button,
@@ -20,7 +24,7 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Contacts> {
} }
pub struct Contacts { pub struct Contacts {
contacts: Entity<Option<Vec<NostrProfile>>>, contacts: Option<Vec<Profile>>,
// Panel // Panel
name: SharedString, name: SharedString,
closable: bool, closable: bool,
@@ -29,48 +33,42 @@ pub struct Contacts {
} }
impl Contacts { impl Contacts {
pub fn new(_window: &mut Window, cx: &mut App) -> Entity<Self> { pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
let contacts = cx.new(|_| None); cx.new(|cx| Self::view(window, cx))
let async_contact = contacts.clone(); }
cx.spawn(async move |cx| { fn view(_window: &mut Window, cx: &mut Context<Self>) -> Self {
cx.spawn(async move |this, cx| {
let client = get_client(); let client = get_client();
let (tx, rx) = oneshot::channel::<Vec<NostrProfile>>();
cx.background_executor() let task: Task<Result<BTreeSet<Profile>, Error>> = cx.background_spawn(async move {
.spawn(async move { let signer = client.signer().await?;
let signer = client.signer().await.unwrap(); let public_key = signer.get_public_key().await?;
let public_key = signer.get_public_key().await.unwrap(); let profiles = client.database().contacts(public_key).await?;
if let Ok(profiles) = client.database().contacts(public_key).await { Ok(profiles)
let members: Vec<NostrProfile> = profiles
.into_iter()
.map(|profile| {
NostrProfile::new(profile.public_key(), profile.metadata())
})
.collect();
_ = tx.send(members);
}
})
.detach();
if let Ok(contacts) = rx.await {
_ = cx.update_entity(&async_contact, |this, cx| {
*this = Some(contacts);
cx.notify();
}); });
if let Ok(contacts) = task.await {
cx.update(|cx| {
this.update(cx, |this, cx| {
this.contacts = Some(contacts.into_iter().collect_vec());
cx.notify();
})
.ok();
})
.ok();
} }
}) })
.detach(); .detach();
cx.new(|cx| Self { Self {
contacts, contacts: None,
name: "Contacts".into(), name: "Contacts".into(),
closable: true, closable: true,
zoomable: true, zoomable: true,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
}) }
} }
} }
@@ -111,7 +109,7 @@ impl Focusable for Contacts {
impl Render for Contacts { impl Render for Contacts {
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 {
div().size_full().pt_2().px_2().map(|this| { div().size_full().pt_2().px_2().map(|this| {
if let Some(contacts) = self.contacts.read(cx).clone() { if let Some(contacts) = self.contacts.clone() {
this.child( this.child(
uniform_list( uniform_list(
cx.entity().clone(), cx.entity().clone(),
@@ -141,9 +139,9 @@ impl Render for Contacts {
.child( .child(
div() div()
.flex_shrink_0() .flex_shrink_0()
.child(img(item.avatar).size_6()), .child(img(item.shared_avatar()).size_6()),
) )
.child(item.name), .child(item.shared_name()),
) )
.hover(|this| { .hover(|this| {
this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)) this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))

View File

@@ -1,7 +1,7 @@
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use account::Account; use account::Account;
use common::utils::create_qr; use common::create_qr;
use global::get_client_keys; use global::get_client_keys;
use gpui::{ use gpui::{
div, img, prelude::FluentBuilder, relative, AnyElement, App, AppContext, Context, Entity, div, img, prelude::FluentBuilder, relative, AnyElement, App, AppContext, Context, Entity,

View File

@@ -1,6 +1,6 @@
use account::Account; use account::Account;
use async_utility::task::spawn; use async_utility::task::spawn;
use common::utils::nip96_upload; use common::nip96_upload;
use global::{constants::IMAGE_SERVICE, get_client}; use global::{constants::IMAGE_SERVICE, get_client};
use gpui::{ use gpui::{
div, img, prelude::FluentBuilder, px, relative, AnyElement, App, AppContext, Context, Entity, div, img, prelude::FluentBuilder, px, relative, AnyElement, App, AppContext, Context, Entity,

View File

@@ -1,5 +1,5 @@
use async_utility::task::spawn; use async_utility::task::spawn;
use common::utils::nip96_upload; use common::nip96_upload;
use global::{constants::IMAGE_SERVICE, get_client}; use global::{constants::IMAGE_SERVICE, get_client};
use gpui::{ use gpui::{
div, img, prelude::FluentBuilder, AnyElement, App, AppContext, Context, Entity, EventEmitter, div, img, prelude::FluentBuilder, AnyElement, App, AppContext, Context, Entity, EventEmitter,

View File

@@ -1,8 +1,9 @@
use anyhow::Error;
use chats::{ use chats::{
room::{Room, RoomKind}, room::{Room, RoomKind},
ChatRegistry, ChatRegistry,
}; };
use common::{profile::NostrProfile, utils::random_name}; use common::{profile::SharedProfile, random_name};
use global::get_client; use global::get_client;
use gpui::{ use gpui::{
div, img, impl_internal_actions, prelude::FluentBuilder, px, relative, uniform_list, App, div, img, impl_internal_actions, prelude::FluentBuilder, px, relative, uniform_list, App,
@@ -14,7 +15,11 @@ use nostr_sdk::prelude::*;
use serde::Deserialize; use serde::Deserialize;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use smol::Timer; use smol::Timer;
use std::{collections::HashSet, rc::Rc, time::Duration}; use std::{
collections::{BTreeSet, HashSet},
rc::Rc,
time::Duration,
};
use ui::{ use ui::{
button::{Button, ButtonRounded}, button::{Button, ButtonRounded},
input::{InputEvent, TextInput}, input::{InputEvent, TextInput},
@@ -33,7 +38,7 @@ impl_internal_actions!(contacts, [SelectContact]);
pub struct Compose { pub struct Compose {
title_input: Entity<TextInput>, title_input: Entity<TextInput>,
user_input: Entity<TextInput>, user_input: Entity<TextInput>,
contacts: Entity<Vec<NostrProfile>>, contacts: Entity<Vec<Profile>>,
selected: Entity<HashSet<PublicKey>>, selected: Entity<HashSet<PublicKey>>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
is_loading: bool, is_loading: bool,
@@ -80,26 +85,17 @@ impl Compose {
}, },
)); ));
let client = get_client();
let (tx, rx) = oneshot::channel::<Vec<NostrProfile>>();
cx.background_spawn(async move {
let signer = client.signer().await.unwrap();
let public_key = signer.get_public_key().await.unwrap();
if let Ok(profiles) = client.database().contacts(public_key).await {
let members: Vec<NostrProfile> = profiles
.into_iter()
.map(|profile| NostrProfile::new(profile.public_key(), profile.metadata()))
.collect();
_ = tx.send(members);
}
})
.detach();
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
if let Ok(contacts) = rx.await { let task: Task<Result<BTreeSet<Profile>, Error>> = cx.background_spawn(async move {
let client = get_client();
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let profiles = client.database().contacts(public_key).await?;
Ok(profiles)
});
if let Ok(contacts) = task.await {
cx.update(|cx| { cx.update(|cx| {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.contacts.update(cx, |this, cx| { this.contacts.update(cx, |this, cx| {
@@ -107,6 +103,7 @@ impl Compose {
cx.notify(); cx.notify();
}); });
}) })
.ok()
}) })
.ok(); .ok();
} }
@@ -174,7 +171,7 @@ impl Compose {
.ok(); .ok();
let chats = ChatRegistry::global(cx); let chats = ChatRegistry::global(cx);
let room = Room::new(&event, RoomKind::Ongoing); let room = Room::new(&event).kind(RoomKind::Ongoing);
chats.update(cx, |chats, cx| { chats.update(cx, |chats, cx| {
match chats.push(room, cx) { match chats.push(room, cx) {
@@ -215,7 +212,7 @@ impl Compose {
// Show loading spinner // Show loading spinner
self.set_loading(true, cx); self.set_loading(true, cx);
let task: Task<Result<NostrProfile, anyhow::Error>> = if content.contains("@") { let task: Task<Result<Profile, anyhow::Error>> = if content.contains("@") {
cx.background_spawn(async move { cx.background_spawn(async move {
let profile = nip05::profile(&content, None).await?; let profile = nip05::profile(&content, None).await?;
let public_key = profile.public_key; let public_key = profile.public_key;
@@ -225,7 +222,7 @@ impl Compose {
.await? .await?
.unwrap_or_default(); .unwrap_or_default();
Ok(NostrProfile::new(public_key, metadata)) Ok(Profile::new(public_key, metadata))
}) })
} else { } else {
let Ok(public_key) = PublicKey::parse(&content) else { let Ok(public_key) = PublicKey::parse(&content) else {
@@ -240,7 +237,7 @@ impl Compose {
.await? .await?
.unwrap_or_default(); .unwrap_or_default();
Ok(NostrProfile::new(public_key, metadata)) Ok(Profile::new(public_key, metadata))
}) })
}; };
@@ -249,7 +246,7 @@ impl Compose {
Ok(profile) => { Ok(profile) => {
cx.update(|window, cx| { cx.update(|window, cx| {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
let public_key = profile.public_key; let public_key = profile.public_key();
this.contacts.update(cx, |this, cx| { this.contacts.update(cx, |this, cx| {
this.insert(0, profile); this.insert(0, profile);
@@ -441,7 +438,7 @@ impl Render for Compose {
for ix in range { for ix in range {
let item = contacts.get(ix).unwrap().clone(); let item = contacts.get(ix).unwrap().clone();
let is_select = selected.contains(&item.public_key); let is_select = selected.contains(&item.public_key());
items.push( items.push(
div() div()
@@ -458,12 +455,10 @@ impl Render for Compose {
.items_center() .items_center()
.gap_2() .gap_2()
.text_xs() .text_xs()
.child( .child(div().flex_shrink_0().child(
div().flex_shrink_0().child( img(item.shared_avatar()).size_6(),
img(item.avatar).size_6(), ))
), .child(item.shared_name()),
)
.child(item.name),
) )
.when(is_select, |this| { .when(is_select, |this| {
this.child( this.child(
@@ -484,7 +479,7 @@ impl Render for Compose {
.on_click(move |_, window, cx| { .on_click(move |_, window, cx| {
window.dispatch_action( window.dispatch_action(
Box::new(SelectContact( Box::new(SelectContact(
item.public_key, item.public_key(),
)), )),
cx, cx,
); );

View File

@@ -138,31 +138,18 @@ impl Sidebar {
for room in rooms { for room in rooms {
let room = room.read(cx); let room = room.read(cx);
let room_id = room.id; let id = room.id;
let ago = room.last_seen().ago(); let ago = room.ago();
let Some(member) = room.first_member() else { let label = room.display_name(cx);
continue; let img = room.display_image(cx).map(img);
};
let label = if room.is_group() { let item = FolderItem::new(id as usize)
room.subject().unwrap_or("Unnamed".into())
} else {
member.name.clone()
};
let img = if !room.is_group() {
Some(img(member.avatar.clone()))
} else {
None
};
let item = FolderItem::new(room_id as usize)
.label(label) .label(label)
.description(ago) .description(ago)
.img(img) .img(img)
.on_click({ .on_click({
cx.listener(move |this, _, window, cx| { cx.listener(move |this, _, window, cx| {
this.open_room(room_id, window, cx); this.open_room(id, window, cx);
}) })
}); });

View File

@@ -5,6 +5,7 @@ edition.workspace = true
publish.workspace = true publish.workspace = true
[dependencies] [dependencies]
account = { path = "../account" }
common = { path = "../common" } common = { path = "../common" }
global = { path = "../global" } global = { path = "../global" }
ui = { path = "../ui" } ui = { path = "../ui" }

View File

@@ -0,0 +1,5 @@
pub(crate) const NOW: &str = "now";
pub(crate) const SECONDS_IN_MINUTE: i64 = 60;
pub(crate) const MINUTES_IN_HOUR: i64 = 60;
pub(crate) const HOURS_IN_DAY: i64 = 24;
pub(crate) const DAYS_IN_MONTH: i64 = 30;

View File

@@ -1,7 +1,7 @@
use std::{cmp::Reverse, collections::HashMap}; use std::{cmp::Reverse, collections::HashMap};
use anyhow::anyhow; use anyhow::{anyhow, Error};
use common::{last_seen::LastSeen, utils::room_hash}; use common::room_hash;
use global::get_client; use global::get_client;
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window}; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window};
use itertools::Itertools; use itertools::Itertools;
@@ -11,6 +11,7 @@ use smallvec::{smallvec, SmallVec};
use crate::room::Room; use crate::room::Room;
mod constants;
pub mod message; pub mod message;
pub mod room; pub mod room;
@@ -22,35 +23,56 @@ struct GlobalChatRegistry(Entity<ChatRegistry>);
impl Global for GlobalChatRegistry {} impl Global for GlobalChatRegistry {}
/// Main registry for managing chat rooms and user profiles
///
/// The ChatRegistry is responsible for:
/// - Managing chat rooms and their states
/// - Tracking user profiles
/// - Loading room data from the lmdb
/// - Handling messages and room creation
pub struct ChatRegistry { pub struct ChatRegistry {
/// Collection of all chat rooms
rooms: Vec<Entity<Room>>, rooms: Vec<Entity<Room>>,
/// Map of user public keys to their profile metadata
profiles: Entity<HashMap<PublicKey, Option<Metadata>>>,
/// Indicates if rooms are currently being loaded
loading: bool, loading: bool,
/// Subscriptions for observing changes
#[allow(dead_code)] #[allow(dead_code)]
subscriptions: SmallVec<[Subscription; 1]>, subscriptions: SmallVec<[Subscription; 1]>,
} }
impl ChatRegistry { impl ChatRegistry {
pub fn global(cx: &mut App) -> Entity<Self> { /// Retrieve the global ChatRegistry instance
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalChatRegistry>().0.clone() cx.global::<GlobalChatRegistry>().0.clone()
} }
/// Set the global ChatRegistry instance
pub fn set_global(state: Entity<Self>, cx: &mut App) { pub fn set_global(state: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalChatRegistry(state)); cx.set_global(GlobalChatRegistry(state));
} }
/// Create a new ChatRegistry instance
fn new(cx: &mut Context<Self>) -> Self { fn new(cx: &mut Context<Self>) -> Self {
let profiles = cx.new(|_| HashMap::new());
let mut subscriptions = smallvec![]; let mut subscriptions = smallvec![];
// Observe new Room creations to collect profile metadata
subscriptions.push(cx.observe_new::<Room>(|this, _, cx| { subscriptions.push(cx.observe_new::<Room>(|this, _, cx| {
let load_metadata = this.load_metadata(cx); let task = this.metadata(cx);
cx.spawn(async move |this, cx| { cx.spawn(async move |_, cx| {
if let Ok(profiles) = load_metadata.await { if let Ok(data) = task.await {
cx.update(|cx| { cx.update(|cx| {
this.update(cx, |this, cx| { for (public_key, metadata) in data.into_iter() {
this.update_members(profiles, cx); Self::global(cx).update(cx, |this, cx| {
this.add_profile(public_key, metadata, cx);
}) })
.ok(); }
}) })
.ok(); .ok();
} }
@@ -61,24 +83,73 @@ impl ChatRegistry {
Self { Self {
rooms: vec![], rooms: vec![],
loading: true, loading: true,
profiles,
subscriptions, subscriptions,
} }
} }
/// Get the global loading status
pub fn loading(&self) -> bool {
self.loading
}
/// Get a room by its ID.
pub fn room(&self, id: &u64, cx: &App) -> Option<Entity<Room>> {
self.rooms
.iter()
.find(|model| model.read(cx).id == *id)
.cloned()
}
/// Get all rooms grouped by their kind.
pub fn rooms(&self, cx: &App) -> HashMap<RoomKind, Vec<&Entity<Room>>> {
let mut groups = HashMap::new();
groups.insert(RoomKind::Ongoing, Vec::new());
groups.insert(RoomKind::Trusted, Vec::new());
groups.insert(RoomKind::Unknown, Vec::new());
for room in self.rooms.iter() {
let kind = room.read(cx).kind;
groups.entry(kind).or_insert_with(Vec::new).push(room);
}
groups
}
/// Get rooms by their kind.
pub fn rooms_by_kind(&self, kind: RoomKind, cx: &App) -> Vec<&Entity<Room>> {
self.rooms
.iter()
.filter(|room| room.read(cx).kind == kind)
.collect()
}
/// Get the IDs of all rooms.
pub fn room_ids(&self, cx: &mut Context<Self>) -> Vec<u64> {
self.rooms.iter().map(|room| room.read(cx).id).collect()
}
/// Load all rooms from the lmdb.
///
/// This method:
/// 1. Fetches all private direct messages from the lmdb
/// 2. Groups them by ID
/// 3. Determines each room's type based on message frequency and trust status
/// 4. Creates Room entities for each unique room
pub fn load_rooms(&mut self, window: &mut Window, cx: &mut Context<Self>) { pub fn load_rooms(&mut self, window: &mut Window, cx: &mut Context<Self>) {
type Rooms = Vec<(Event, usize, bool)>;
let task: Task<Result<Rooms, Error>> = cx.background_spawn(async move {
let client = get_client(); let client = get_client();
let room_ids = self.room_ids(cx);
type LoadResult = Result<Vec<(Event, usize, bool)>, Error>;
let task: Task<LoadResult> = cx.background_spawn(async move {
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
// Get messages sent by the user
let send = Filter::new() let send = Filter::new()
.kind(Kind::PrivateDirectMessage) .kind(Kind::PrivateDirectMessage)
.author(public_key); .author(public_key);
// Get messages received by the user
let recv = Filter::new() let recv = Filter::new()
.kind(Kind::PrivateDirectMessage) .kind(Kind::PrivateDirectMessage)
.pubkey(public_key); .pubkey(public_key);
@@ -89,13 +160,13 @@ impl ChatRegistry {
let mut room_map: HashMap<u64, (Event, usize, bool)> = HashMap::new(); let mut room_map: HashMap<u64, (Event, usize, bool)> = HashMap::new();
// Process each event and group by room hash
for event in events for event in events
.into_iter() .into_iter()
.filter(|ev| ev.tags.public_keys().peekable().peek().is_some()) .filter(|ev| ev.tags.public_keys().peekable().peek().is_some())
{ {
let hash = room_hash(&event); let hash = room_hash(&event);
if !room_ids.iter().any(|id| id == &hash) {
let filter = Filter::new().kind(Kind::ContactList).pubkey(event.pubkey); let filter = Filter::new().kind(Kind::ContactList).pubkey(event.pubkey);
let is_trust = client.database().count(filter).await? >= 1; let is_trust = client.database().count(filter).await? >= 1;
@@ -107,8 +178,8 @@ impl ChatRegistry {
}) })
.or_insert((event, 1, is_trust)); .or_insert((event, 1, is_trust));
} }
}
// Sort rooms by creation date (newest first)
let result: Vec<(Event, usize, bool)> = room_map let result: Vec<(Event, usize, bool)> = room_map
.into_values() .into_values()
.sorted_by_key(|(ev, _, _)| Reverse(ev.created_at)) .sorted_by_key(|(ev, _, _)| Reverse(ev.created_at))
@@ -119,9 +190,14 @@ impl ChatRegistry {
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
if let Ok(events) = task.await { if let Ok(events) = task.await {
cx.update(|_, cx| {
this.update(cx, |this, cx| {
let ids = this.room_ids(cx);
let rooms: Vec<Entity<Room>> = events let rooms: Vec<Entity<Room>> = events
.into_iter() .into_iter()
.map(|(event, count, trusted)| { .filter_map(|(event, count, trusted)| {
let hash = room_hash(&event);
if !ids.iter().any(|this| this == &hash) {
let kind = if count > 2 { let kind = if count > 2 {
// If frequency count is greater than 2, mark this room as ongoing // If frequency count is greater than 2, mark this room as ongoing
RoomKind::Ongoing RoomKind::Ongoing
@@ -130,15 +206,15 @@ impl ChatRegistry {
} else { } else {
RoomKind::Unknown RoomKind::Unknown
}; };
Some(cx.new(|_| Room::new(&event).kind(kind)))
cx.new(|_| Room::new(&event, kind)).unwrap() } else {
None
}
}) })
.collect(); .collect();
cx.update(|_, cx| {
this.update(cx, |this, cx| {
this.rooms.extend(rooms); this.rooms.extend(rooms);
this.rooms.sort_by_key(|r| Reverse(r.read(cx).last_seen())); this.rooms.sort_by_key(|r| Reverse(r.read(cx).created_at));
this.loading = false; this.loading = false;
cx.notify(); cx.notify();
@@ -151,48 +227,40 @@ impl ChatRegistry {
.detach(); .detach();
} }
/// Get the IDs of all rooms. /// Add a user profile to the registry
pub fn room_ids(&self, cx: &mut Context<Self>) -> Vec<u64> { ///
self.rooms.iter().map(|room| room.read(cx).id).collect() /// Only adds the profile if it doesn't already exist or is currently none
pub fn add_profile(
&mut self,
public_key: PublicKey,
metadata: Option<Metadata>,
cx: &mut Context<Self>,
) {
self.profiles.update(cx, |this, _cx| {
this.entry(public_key)
.and_modify(|entry| {
if entry.is_none() {
*entry = metadata.clone();
}
})
.or_insert_with(|| metadata);
});
} }
/// Get all rooms. /// Get a user profile by public key
pub fn rooms(&self, cx: &App) -> HashMap<RoomKind, Vec<&Entity<Room>>> { pub fn profile(&self, public_key: &PublicKey, cx: &App) -> Profile {
let mut groups = HashMap::new(); let metadata = if let Some(profile) = self.profiles.read(cx).get(public_key) {
groups.insert(RoomKind::Ongoing, Vec::new()); profile.clone().unwrap_or_default()
groups.insert(RoomKind::Trusted, Vec::new()); } else {
groups.insert(RoomKind::Unknown, Vec::new()); Metadata::default()
};
for room in self.rooms.iter() { Profile::new(*public_key, metadata)
let kind = room.read(cx).kind();
groups.entry(kind).or_insert_with(Vec::new).push(room);
} }
groups /// Add a new room to the registry
} ///
/// Returns an error if the room already exists
/// Get rooms by their kind.
pub fn rooms_by_kind(&self, kind: RoomKind, cx: &App) -> Vec<&Entity<Room>> {
self.rooms
.iter()
.filter(|room| room.read(cx).kind() == kind)
.collect()
}
/// Get the loading status of the rooms.
pub fn loading(&self) -> bool {
self.loading
}
/// Get a room by its ID.
pub fn get(&self, id: &u64, cx: &App) -> Option<Entity<Room>> {
self.rooms
.iter()
.find(|model| model.read(cx).id == *id)
.cloned()
}
/// Push a room to the list.
pub fn push(&mut self, room: Room, cx: &mut Context<Self>) -> Result<(), anyhow::Error> { pub fn push(&mut self, room: Room, cx: &mut Context<Self>) -> Result<(), anyhow::Error> {
let room = cx.new(|_| room); let room = cx.new(|_| room);
@@ -210,21 +278,24 @@ impl ChatRegistry {
} }
} }
/// Push a message to a room. /// Push a new message to a room
///
/// If the room doesn't exist, it will be created.
/// Updates room ordering based on the most recent messages.
pub fn push_message(&mut self, event: Event, window: &mut Window, cx: &mut Context<Self>) { pub fn push_message(&mut self, event: Event, window: &mut Window, cx: &mut Context<Self>) {
let id = room_hash(&event); let id = room_hash(&event);
if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) { if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) {
room.update(cx, |this, cx| { room.update(cx, |this, cx| {
this.set_last_seen(LastSeen(event.created_at), cx); this.created_at(event.created_at, cx);
this.emit_message(event, window, cx); this.emit_message(event, window, cx);
}); });
// Re-sort rooms by last seen // Re-sort rooms by last seen
self.rooms self.rooms
.sort_by_key(|room| Reverse(room.read(cx).last_seen())); .sort_by_key(|room| Reverse(room.read(cx).created_at));
} else { } else {
let new_room = cx.new(|_| Room::new(&event, RoomKind::default())); let new_room = cx.new(|_| Room::new(&event));
// Push the new room to the front of the list // Push the new room to the front of the list
self.rooms.insert(0, new_room); self.rooms.insert(0, new_room);

View File

@@ -1,36 +1,101 @@
use common::{last_seen::LastSeen, profile::NostrProfile}; use chrono::{Local, TimeZone};
use gpui::SharedString; use gpui::SharedString;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
/// # Message
///
/// Represents a message in the application.
///
/// ## Fields
///
/// - `id`: The unique identifier for the message
/// - `content`: The text content of the message
/// - `author`: Profile information about who created the message
/// - `mentions`: List of profiles mentioned in the message
/// - `created_at`: Timestamp when the message was created
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Message { pub struct Message {
pub id: EventId, pub id: EventId,
pub content: String, pub content: String,
pub author: NostrProfile, pub author: Profile,
pub mentions: Vec<NostrProfile>, pub mentions: Vec<Profile>,
pub created_at: LastSeen, pub created_at: Timestamp,
} }
impl Message { impl Message {
pub fn new( /// Creates a new message with the provided details
id: EventId, ///
content: String, /// # Arguments
author: NostrProfile, ///
mentions: Vec<NostrProfile>, /// * `id` - Unique event identifier
created_at: Timestamp, /// * `content` - Message text content
) -> Self { /// * `author` - Profile of the message author
let created_at = LastSeen(created_at); /// * `created_at` - When the message was created
///
/// # Returns
///
/// A new `Message` instance
pub fn new(id: EventId, content: String, author: Profile, created_at: Timestamp) -> Self {
Self { Self {
id, id,
content, content,
author, author,
mentions,
created_at, created_at,
} mentions: vec![],
} }
} }
/// Adds or replaces mentions in the message
///
/// # Arguments
///
/// * `mentions` - New list of mentioned profiles
///
/// # Returns
///
/// The same message with updated mentions
pub fn with_mentions(mut self, mentions: impl IntoIterator<Item = Profile>) -> Self {
self.mentions.extend(mentions);
self
}
/// Formats the message timestamp as a human-readable relative time
///
/// # Returns
///
/// A formatted string like "Today at 12:30 PM", "Yesterday at 3:45 PM",
/// or a date and time for older messages
pub fn ago(&self) -> SharedString {
let input_time = match Local.timestamp_opt(self.created_at.as_u64() as i64, 0) {
chrono::LocalResult::Single(time) => time,
_ => return "Invalid timestamp".into(),
};
let now = Local::now();
let input_date = input_time.date_naive();
let now_date = now.date_naive();
let yesterday_date = (now - chrono::Duration::days(1)).date_naive();
let time_format = input_time.format("%H:%M %p");
match input_date {
date if date == now_date => format!("Today at {time_format}"),
date if date == yesterday_date => format!("Yesterday at {time_format}"),
_ => format!("{}, {time_format}", input_time.format("%d/%m/%y")),
}
.into()
}
}
/// # RoomMessage
///
/// Represents different types of messages that can appear in a room.
///
/// ## Variants
///
/// - `User`: A message sent by a user
/// - `System`: A message generated by the system
/// - `Announcement`: A special message type used for room announcements
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum RoomMessage { pub enum RoomMessage {
/// User message /// User message
@@ -43,14 +108,37 @@ pub enum RoomMessage {
} }
impl RoomMessage { impl RoomMessage {
/// Creates a new user message
///
/// # Arguments
///
/// * `message` - The message content
///
/// # Returns
///
/// A `RoomMessage::User` variant
pub fn user(message: Message) -> Self { pub fn user(message: Message) -> Self {
Self::User(Box::new(message)) Self::User(Box::new(message))
} }
/// Creates a new system message
///
/// # Arguments
///
/// * `content` - The system message content
///
/// # Returns
///
/// A `RoomMessage::System` variant
pub fn system(content: SharedString) -> Self { pub fn system(content: SharedString) -> Self {
Self::System(content) Self::System(content)
} }
/// Creates a new announcement placeholder
///
/// # Returns
///
/// A `RoomMessage::Announcement` variant
pub fn announcement() -> Self { pub fn announcement() -> Self {
Self::Announcement Self::Announcement
} }

View File

@@ -1,18 +1,19 @@
use std::{collections::HashSet, sync::Arc}; use std::sync::Arc;
use account::Account;
use anyhow::Error; use anyhow::Error;
use common::{ use chrono::{Local, TimeZone};
last_seen::LastSeen, use common::{compare, profile::SharedProfile, room_hash};
profile::NostrProfile,
utils::{compare, room_hash},
};
use global::get_client; use global::get_client;
use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task, Window}; use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task, Window};
use itertools::Itertools; use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use smallvec::SmallVec;
use crate::message::{Message, RoomMessage}; use crate::{
constants::{DAYS_IN_MONTH, HOURS_IN_DAY, MINUTES_IN_HOUR, NOW, SECONDS_IN_MINUTE},
message::{Message, RoomMessage},
ChatRegistry,
};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct IncomingEvent { pub struct IncomingEvent {
@@ -29,15 +30,13 @@ pub enum RoomKind {
pub struct Room { pub struct Room {
pub id: u64, pub id: u64,
pub last_seen: LastSeen, pub created_at: Timestamp,
/// Subject of the room /// Subject of the room
pub subject: Option<SharedString>, pub subject: Option<SharedString>,
/// All members of the room /// All members of the room
pub members: Arc<SmallVec<[NostrProfile; 2]>>, pub members: Arc<Vec<PublicKey>>,
/// Kind /// Kind
pub kind: RoomKind, pub kind: RoomKind,
/// All public keys of the room members
pubkeys: Vec<PublicKey>,
} }
impl EventEmitter<IncomingEvent> for Room {} impl EventEmitter<IncomingEvent> for Room {}
@@ -49,10 +48,25 @@ impl PartialEq for Room {
} }
impl Room { impl Room {
/// Create a new room from an Nostr Event /// Creates a new Room instance from a Nostr event
pub fn new(event: &Event, kind: RoomKind) -> Self { ///
/// # Arguments
///
/// * `event` - The Nostr event containing chat information
///
/// # Returns
///
/// A new Room instance with information extracted from the event
pub fn new(event: &Event) -> Self {
let id = room_hash(event); let id = room_hash(event);
let last_seen = LastSeen(event.created_at); let created_at = event.created_at;
// Get all pubkeys from the event's tags
let mut pubkeys: Vec<PublicKey> = event.tags.public_keys().cloned().collect();
pubkeys.push(event.pubkey);
// Convert pubkeys into members
let members = Arc::new(pubkeys.into_iter().unique().sorted().collect());
// Get the subject from the event's tags // Get the subject from the event's tags
let subject = if let Some(tag) = event.tags.find(TagKind::Subject) { let subject = if let Some(tag) = event.tags.find(TagKind::Subject) {
@@ -61,112 +75,264 @@ impl Room {
None None
}; };
// Get all public keys from the event's tags
let mut pubkeys = vec![];
pubkeys.extend(event.tags.public_keys().collect::<HashSet<_>>());
pubkeys.push(event.pubkey);
Self { Self {
id, id,
last_seen, created_at,
subject, subject,
kind, members,
members: Arc::new(SmallVec::with_capacity(pubkeys.len())), kind: RoomKind::Unknown,
pubkeys,
} }
} }
/// Get room's id /// Sets the kind of the room
pub fn id(&self) -> u64 { ///
self.id /// # Arguments
///
/// * `kind` - The kind of room to set
///
/// # Returns
///
/// The room with the updated kind
pub fn kind(mut self, kind: RoomKind) -> Self {
self.kind = kind;
self
} }
/// Get room's member by public key /// Calculates a human-readable representation of the time passed since room creation
pub fn member(&self, public_key: &PublicKey) -> Option<NostrProfile> { ///
self.members /// # Returns
.iter() ///
.find(|m| &m.public_key == public_key) /// A SharedString representing the relative time since room creation:
.cloned() /// - "now" for less than a minute
} /// - "Xm" for minutes
/// - "Xh" for hours
/// Get room's first member's public key /// - "Xd" for days
pub fn first_member(&self) -> Option<&NostrProfile> { /// - Month and day (e.g. "Jan 15") for older dates
self.members.first()
}
/// Collect room's member's public keys
pub fn public_keys(&self) -> Vec<PublicKey> {
self.pubkeys.clone()
}
/// Get room's display name
pub fn subject(&self) -> Option<SharedString> {
self.subject.clone()
}
/// Get room's kind
pub fn kind(&self) -> RoomKind {
self.kind
}
/// Determine if room is a group
pub fn is_group(&self) -> bool {
self.members.len() > 2
}
/// Get room's last seen
pub fn last_seen(&self) -> LastSeen {
self.last_seen
}
/// Set room's last seen
pub fn set_last_seen(&mut self, last_seen: LastSeen, cx: &mut Context<Self>) {
self.last_seen = last_seen;
cx.notify();
}
/// Get room's last seen as ago format
pub fn ago(&self) -> SharedString { pub fn ago(&self) -> SharedString {
self.last_seen.ago() let input_time = match Local.timestamp_opt(self.created_at.as_u64() as i64, 0) {
chrono::LocalResult::Single(time) => time,
_ => return "1m".into(),
};
let now = Local::now();
let duration = now.signed_duration_since(input_time);
match duration {
d if d.num_seconds() < SECONDS_IN_MINUTE => NOW.into(),
d if d.num_minutes() < MINUTES_IN_HOUR => format!("{}m", d.num_minutes()),
d if d.num_hours() < HOURS_IN_DAY => format!("{}h", d.num_hours()),
d if d.num_days() < DAYS_IN_MONTH => format!("{}d", d.num_days()),
_ => input_time.format("%b %d").to_string(),
}
.into()
} }
pub fn update_members(&mut self, profiles: Vec<NostrProfile>, cx: &mut Context<Self>) { /// Gets the profile for a specific public key
// Update the room's name if it's not already set ///
if self.subject.is_none() { /// # Arguments
// Merge all members into a single name ///
/// * `public_key` - The public key to get the profile for
/// * `cx` - The App context
///
/// # Returns
///
/// The Profile associated with the given public key
pub fn profile_by_pubkey(&self, public_key: &PublicKey, cx: &App) -> Profile {
ChatRegistry::global(cx).read(cx).profile(public_key, cx)
}
/// Gets the first member in the room that isn't the current user
///
/// # Arguments
///
/// * `cx` - The App context
///
/// # Returns
///
/// The Profile of the first member in the room
pub fn first_member(&self, cx: &App) -> Profile {
let account = Account::global(cx).read(cx);
let profile = account.profile.clone().unwrap();
if let Some(public_key) = self
.members
.iter()
.filter(|&pubkey| pubkey != &profile.public_key())
.collect::<Vec<_>>()
.first()
{
self.profile_by_pubkey(public_key, cx)
} else {
profile
}
}
/// Gets all avatars for members in the room
///
/// # Arguments
///
/// * `cx` - The App context
///
/// # Returns
///
/// A vector of SharedString containing all members' avatars
pub fn avatars(&self, cx: &App) -> Vec<SharedString> {
let profiles: Vec<Profile> = self
.members
.iter()
.map(|pubkey| ChatRegistry::global(cx).read(cx).profile(pubkey, cx))
.collect();
profiles
.iter()
.map(|member| member.shared_avatar())
.collect()
}
/// Gets a formatted string of member names
///
/// # Arguments
///
/// * `cx` - The App context
///
/// # Returns
///
/// A SharedString containing formatted member names:
/// - For a group chat: "name1, name2, +X" where X is the number of additional members
/// - For a direct message: just the name of the other person
pub fn names(&self, cx: &App) -> SharedString {
if self.is_group() {
let profiles = self
.members
.iter()
.map(|pubkey| ChatRegistry::global(cx).read(cx).profile(pubkey, cx))
.collect::<Vec<_>>();
let mut name = profiles let mut name = profiles
.iter() .iter()
.take(2) .take(2)
.map(|profile| profile.name.to_string()) .map(|profile| profile.shared_name())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", "); .join(", ");
// Create a specific name for group
if profiles.len() > 2 { if profiles.len() > 2 {
name = format!("{}, +{}", name, profiles.len() - 2); name = format!("{}, +{}", name, profiles.len() - 2);
} }
self.subject = Some(name.into()); name.into()
}; } else {
self.first_member(cx).shared_name()
}
}
// Update the room's members /// Gets the display name for the room
self.members = Arc::new(profiles.into()); ///
/// # Arguments
///
/// * `cx` - The App context
///
/// # Returns
///
/// A SharedString representing the display name:
/// - The subject of the room if it exists
/// - Otherwise, the formatted names of the members
pub fn display_name(&self, cx: &App) -> SharedString {
if let Some(subject) = self.subject.as_ref() {
subject.clone()
} else {
self.names(cx)
}
}
/// Gets the display image for the room
///
/// # Arguments
///
/// * `cx` - The App context
///
/// # Returns
///
/// An Option<SharedString> containing the avatar:
/// - For a direct message: the other person's avatar
/// - For a group chat: None
pub fn display_image(&self, cx: &App) -> Option<SharedString> {
if !self.is_group() {
Some(self.first_member(cx).shared_avatar())
} else {
None
}
}
/// Checks if the room is a group chat
///
/// # Returns
///
/// true if the room has more than 2 members, false otherwise
pub fn is_group(&self) -> bool {
self.members.len() > 2
}
/// Updates the creation timestamp of the room
///
/// # Arguments
///
/// * `created_at` - The new Timestamp to set
/// * `cx` - The context to notify about the update
pub fn created_at(&mut self, created_at: Timestamp, cx: &mut Context<Self>) {
self.created_at = created_at;
cx.notify(); cx.notify();
} }
/// Verify messaging_relays for all room's members /// Fetches metadata for all members in the room
///
/// # Arguments
///
/// * `cx` - The context for the background task
///
/// # Returns
///
/// A Task that resolves to Result<Vec<(PublicKey, Option<Metadata>)>, Error>
#[allow(clippy::type_complexity)]
pub fn metadata(
&self,
cx: &mut Context<Self>,
) -> Task<Result<Vec<(PublicKey, Option<Metadata>)>, Error>> {
let client = get_client();
let public_keys = self.members.clone();
cx.background_spawn(async move {
let mut output = vec![];
for public_key in public_keys.iter() {
let metadata = client.database().metadata(*public_key).await?;
output.push((*public_key, metadata));
}
Ok(output)
})
}
/// Checks which members have inbox relays set up
///
/// # Arguments
///
/// * `cx` - The App context
///
/// # Returns
///
/// A Task that resolves to Result<Vec<(PublicKey, bool)>, Error> where
/// the boolean indicates if the member has inbox relays configured
pub fn messaging_relays(&self, cx: &App) -> Task<Result<Vec<(PublicKey, bool)>, Error>> { pub fn messaging_relays(&self, cx: &App) -> Task<Result<Vec<(PublicKey, bool)>, Error>> {
let client = get_client(); let client = get_client();
let pubkeys = self.public_keys(); let pubkeys = Arc::clone(&self.members);
cx.background_spawn(async move { cx.background_spawn(async move {
let mut result = Vec::with_capacity(pubkeys.len()); let mut result = Vec::with_capacity(pubkeys.len());
for pubkey in pubkeys.into_iter() { for pubkey in pubkeys.iter() {
let filter = Filter::new() let filter = Filter::new()
.kind(Kind::InboxRelays) .kind(Kind::InboxRelays)
.author(pubkey) .author(*pubkey)
.limit(1); .limit(1);
let is_ready = client let is_ready = client
@@ -177,17 +343,27 @@ impl Room {
.and_then(|events| events.first_owned()) .and_then(|events| events.first_owned())
.is_some(); .is_some();
result.push((pubkey, is_ready)); result.push((*pubkey, is_ready));
} }
Ok(result) Ok(result)
}) })
} }
/// Send message to all room's members /// Sends a message to all members in the room
///
/// # Arguments
///
/// * `content` - The content of the message to send
/// * `cx` - The App context
///
/// # Returns
///
/// A Task that resolves to Result<Vec<String>, Error> where the
/// strings contain error messages for any failed sends
pub fn send_message(&self, content: String, cx: &App) -> Task<Result<Vec<String>, Error>> { pub fn send_message(&self, content: String, cx: &App) -> Task<Result<Vec<String>, Error>> {
let client = get_client(); let client = get_client();
let pubkeys = self.public_keys(); let pubkeys = self.members.clone();
cx.background_spawn(async move { cx.background_spawn(async move {
let signer = client.signer().await?; let signer = client.signer().await?;
@@ -218,48 +394,29 @@ impl Room {
}) })
} }
/// Load metadata for all members /// Loads all messages for this room from the database
pub fn load_metadata(&self, cx: &mut Context<Self>) -> Task<Result<Vec<NostrProfile>, Error>> { ///
let client = get_client(); /// # Arguments
let pubkeys = self.public_keys(); ///
/// * `cx` - The App context
cx.background_spawn(async move { ///
let signer = client.signer().await?; /// # Returns
let signer_pubkey = signer.get_public_key().await?; ///
let mut profiles = Vec::with_capacity(pubkeys.len()); /// A Task that resolves to Result<Vec<RoomMessage>, Error> containing
/// all messages for this room
for public_key in pubkeys.into_iter() {
let metadata = client
.database()
.metadata(public_key)
.await?
.unwrap_or_default();
// Convert metadata to profile
let profile = NostrProfile::new(public_key, metadata);
if public_key == signer_pubkey {
// Room's owner always push to the end of the vector
profiles.push(profile);
} else {
profiles.insert(0, profile);
}
}
Ok(profiles)
})
}
/// Load room messages
pub fn load_messages(&self, cx: &App) -> Task<Result<Vec<RoomMessage>, Error>> { pub fn load_messages(&self, cx: &App) -> Task<Result<Vec<RoomMessage>, Error>> {
let client = get_client(); let client = get_client();
let pubkeys = self.public_keys(); let pubkeys = Arc::clone(&self.members);
let members = Arc::clone(&self.members);
let profiles: Vec<Profile> = pubkeys
.iter()
.map(|pubkey| ChatRegistry::global(cx).read(cx).profile(pubkey, cx))
.collect();
let filter = Filter::new() let filter = Filter::new()
.kind(Kind::PrivateDirectMessage) .kind(Kind::PrivateDirectMessage)
.authors(pubkeys.clone()) .authors(pubkeys.to_vec())
.pubkeys(pubkeys.clone()); .pubkeys(pubkeys.to_vec());
cx.background_spawn(async move { cx.background_spawn(async move {
let mut messages = vec![]; let mut messages = vec![];
@@ -282,14 +439,16 @@ impl Room {
for event in events.into_iter() { for event in events.into_iter() {
let mut mentions = vec![]; let mut mentions = vec![];
let id = event.id;
let created_at = event.created_at;
let content = event.content.clone(); let content = event.content.clone();
let tokens = parser.parse(&content); let tokens = parser.parse(&content);
let author = members let author = profiles
.iter() .iter()
.find(|profile| profile.public_key == event.pubkey) .find(|profile| profile.public_key() == event.pubkey)
.cloned() .cloned()
.unwrap_or_else(|| NostrProfile::new(event.pubkey, Metadata::default())); .unwrap_or_else(|| Profile::new(event.pubkey, Metadata::default()));
let pubkey_tokens = tokens let pubkey_tokens = tokens
.filter_map(|token| match token { .filter_map(|token| match token {
@@ -303,22 +462,16 @@ impl Room {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for pubkey in pubkey_tokens { for pubkey in pubkey_tokens {
if let Some(profile) = mentions.push(
members.iter().find(|profile| profile.public_key == pubkey) profiles
{ .iter()
mentions.push(profile.clone()); .find(|profile| profile.public_key() == pubkey)
} else { .cloned()
let metadata = client .unwrap_or_else(|| Profile::new(pubkey, Metadata::default())),
.database() );
.metadata(pubkey)
.await?
.unwrap_or_default();
mentions.push(NostrProfile::new(pubkey, metadata));
}
} }
let message = Message::new(event.id, content, author, mentions, event.created_at); let message = Message::new(id, content, author, created_at).with_mentions(mentions);
let room_message = RoomMessage::user(message); let room_message = RoomMessage::user(message);
messages.push(room_message); messages.push(room_message);
@@ -328,22 +481,37 @@ impl Room {
}) })
} }
/// Emit message to GPUI /// Emits a message event to the GPUI
///
/// # Arguments
///
/// * `event` - The Nostr event to emit
/// * `window` - The Window to emit the event to
/// * `cx` - The context for the room
///
/// # Effects
///
/// Processes the event and emits an IncomingEvent to the UI when complete
pub fn emit_message(&self, event: Event, window: &mut Window, cx: &mut Context<Self>) { pub fn emit_message(&self, event: Event, window: &mut Window, cx: &mut Context<Self>) {
let client = get_client(); let pubkeys = self.members.clone();
let members = Arc::clone(&self.members); let profiles: Vec<Profile> = pubkeys
.iter()
.map(|pubkey| ChatRegistry::global(cx).read(cx).profile(pubkey, cx))
.collect();
let task: Task<Result<RoomMessage, Error>> = cx.background_spawn(async move { let task: Task<Result<RoomMessage, Error>> = cx.background_spawn(async move {
let parser = NostrParser::new(); let parser = NostrParser::new();
let id = event.id;
let created_at = event.created_at;
let content = event.content.clone(); let content = event.content.clone();
let tokens = parser.parse(&content); let tokens = parser.parse(&content);
let mut mentions = vec![]; let mut mentions = vec![];
let author = members let author = profiles
.iter() .iter()
.find(|profile| profile.public_key == event.pubkey) .find(|profile| profile.public_key() == event.pubkey)
.cloned() .cloned()
.unwrap_or_else(|| NostrProfile::new(event.pubkey, Metadata::default())); .unwrap_or_else(|| Profile::new(event.pubkey, Metadata::default()));
let pubkey_tokens = tokens let pubkey_tokens = tokens
.filter_map(|token| match token { .filter_map(|token| match token {
@@ -357,23 +525,16 @@ impl Room {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for pubkey in pubkey_tokens { for pubkey in pubkey_tokens {
if let Some(profile) = members mentions.push(
profiles
.iter() .iter()
.find(|profile| profile.public_key == event.pubkey) .find(|profile| profile.public_key() == pubkey)
{ .cloned()
mentions.push(profile.clone()); .unwrap_or_else(|| Profile::new(pubkey, Metadata::default())),
} else { );
let metadata = client
.database()
.metadata(pubkey)
.await?
.unwrap_or_default();
mentions.push(NostrProfile::new(pubkey, metadata));
}
} }
let message = Message::new(event.id, content, author, mentions, event.created_at); let message = Message::new(id, content, author, created_at).with_mentions(mentions);
let room_message = RoomMessage::user(message); let room_message = RoomMessage::user(message);
Ok(room_message) Ok(room_message)

View File

@@ -1,57 +0,0 @@
use chrono::{Local, TimeZone};
use gpui::SharedString;
use nostr_sdk::prelude::*;
const NOW: &str = "now";
const SECONDS_IN_MINUTE: i64 = 60;
const MINUTES_IN_HOUR: i64 = 60;
const HOURS_IN_DAY: i64 = 24;
const DAYS_IN_MONTH: i64 = 30;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct LastSeen(pub Timestamp);
impl LastSeen {
pub fn ago(&self) -> SharedString {
let now = Local::now();
let input_time = match Local.timestamp_opt(self.0.as_u64() as i64, 0) {
chrono::LocalResult::Single(time) => time,
_ => return "Invalid timestamp".into(),
};
let duration = now.signed_duration_since(input_time);
match duration {
d if d.num_seconds() < SECONDS_IN_MINUTE => NOW.into(),
d if d.num_minutes() < MINUTES_IN_HOUR => format!("{}m", d.num_minutes()),
d if d.num_hours() < HOURS_IN_DAY => format!("{}h", d.num_hours()),
d if d.num_days() < DAYS_IN_MONTH => format!("{}d", d.num_days()),
_ => input_time.format("%b %d").to_string(),
}
.into()
}
pub fn human_readable(&self) -> SharedString {
let now = Local::now();
let input_time = match Local.timestamp_opt(self.0.as_u64() as i64, 0) {
chrono::LocalResult::Single(time) => time,
_ => return "Invalid timestamp".into(),
};
let input_date = input_time.date_naive();
let now_date = now.date_naive();
let yesterday_date = (now - chrono::Duration::days(1)).date_naive();
let time_format = input_time.format("%H:%M %p");
match input_date {
date if date == now_date => format!("Today at {time_format}"),
date if date == yesterday_date => format!("Yesterday at {time_format}"),
_ => format!("{}, {time_format}", input_time.format("%d/%m/%y")),
}
.into()
}
pub fn set(&mut self, created_at: Timestamp) {
self.0 = created_at
}
}

View File

@@ -1,3 +1,78 @@
pub mod last_seen; use std::{
collections::HashSet,
hash::{DefaultHasher, Hash, Hasher},
sync::Arc,
};
use anyhow::Context;
use global::constants::NIP96_SERVER;
use gpui::Image;
use itertools::Itertools;
use nostr_sdk::prelude::*;
use qrcode_generator::QrCodeEcc;
use rnglib::{Language, RNG};
pub mod profile; pub mod profile;
pub mod utils;
pub async fn nip96_upload(client: &Client, file: Vec<u8>) -> anyhow::Result<Url, anyhow::Error> {
let signer = client.signer().await?;
let server_url = Url::parse(NIP96_SERVER)?;
let config: ServerConfig = nip96::get_server_config(server_url, None).await?;
let url = nip96::upload_data(&signer, &config, file, None, None).await?;
Ok(url)
}
pub fn room_hash(event: &Event) -> u64 {
let mut hasher = DefaultHasher::new();
let mut pubkeys: Vec<&PublicKey> = vec![];
// Add all public keys from event
pubkeys.push(&event.pubkey);
pubkeys.extend(event.tags.public_keys().collect::<Vec<_>>());
// Generate unique hash
pubkeys
.into_iter()
.unique()
.sorted()
.collect::<Vec<_>>()
.hash(&mut hasher);
hasher.finish()
}
pub fn device_pubkey(event: &Event) -> Result<PublicKey, anyhow::Error> {
let n_tag = event.tags.find(TagKind::custom("n")).context("Invalid")?;
let hex = n_tag.content().context("Invalid")?;
let pubkey = PublicKey::parse(hex)?;
Ok(pubkey)
}
pub fn random_name(length: usize) -> String {
let rng = RNG::from(&Language::Roman);
rng.generate_names(length, true).join("-").to_lowercase()
}
pub fn create_qr(data: &str) -> Result<Arc<Image>, anyhow::Error> {
let qr = qrcode_generator::to_png_to_vec_from_str(data, QrCodeEcc::Medium, 256)?;
let img = Arc::new(Image {
format: gpui::ImageFormat::Png,
bytes: qr.clone(),
id: 1,
});
Ok(img)
}
pub fn compare<T>(a: &[T], b: &[T]) -> bool
where
T: Eq + Hash,
{
let a: HashSet<_> = a.iter().collect();
let b: HashSet<_> = b.iter().collect();
a == b
}

View File

@@ -2,27 +2,14 @@ use global::constants::IMAGE_SERVICE;
use gpui::SharedString; use gpui::SharedString;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
#[derive(Debug, Clone, PartialEq, Eq)] pub trait SharedProfile {
pub struct NostrProfile { fn shared_avatar(&self) -> SharedString;
pub public_key: PublicKey, fn shared_name(&self) -> SharedString;
pub avatar: SharedString,
pub name: SharedString,
} }
impl NostrProfile { impl SharedProfile for Profile {
pub fn new(public_key: PublicKey, metadata: Metadata) -> Self { fn shared_avatar(&self) -> SharedString {
let name = Self::extract_name(&public_key, &metadata); self.metadata()
let avatar = Self::extract_avatar(&metadata);
Self {
public_key,
name,
avatar,
}
}
fn extract_avatar(metadata: &Metadata) -> SharedString {
metadata
.picture .picture
.as_ref() .as_ref()
.filter(|picture| !picture.is_empty()) .filter(|picture| !picture.is_empty())
@@ -36,20 +23,20 @@ impl NostrProfile {
.unwrap_or_else(|| "brand/avatar.png".into()) .unwrap_or_else(|| "brand/avatar.png".into())
} }
fn extract_name(public_key: &PublicKey, metadata: &Metadata) -> SharedString { fn shared_name(&self) -> SharedString {
if let Some(display_name) = metadata.display_name.as_ref() { if let Some(display_name) = self.metadata().display_name.as_ref() {
if !display_name.is_empty() { if !display_name.is_empty() {
return display_name.into(); return display_name.into();
} }
} }
if let Some(name) = metadata.name.as_ref() { if let Some(name) = self.metadata().name.as_ref() {
if !name.is_empty() { if !name.is_empty() {
return name.into(); return name.into();
} }
} }
let pubkey = public_key.to_hex(); let pubkey = self.public_key().to_hex();
format!("{}:{}", &pubkey[0..4], &pubkey[pubkey.len() - 4..]).into() format!("{}:{}", &pubkey[0..4], &pubkey[pubkey.len() - 4..]).into()
} }

View File

@@ -1,82 +0,0 @@
use anyhow::Context;
use global::constants::NIP96_SERVER;
use gpui::Image;
use itertools::Itertools;
use nostr_sdk::prelude::*;
use qrcode_generator::QrCodeEcc;
use rnglib::{Language, RNG};
use std::{
collections::HashSet,
hash::{DefaultHasher, Hash, Hasher},
sync::Arc,
};
pub async fn nip96_upload(client: &Client, file: Vec<u8>) -> anyhow::Result<Url, anyhow::Error> {
let signer = client.signer().await?;
let server_url = Url::parse(NIP96_SERVER)?;
let config: ServerConfig = nip96::get_server_config(server_url, None).await?;
let url = nip96::upload_data(&signer, &config, file, None, None).await?;
Ok(url)
}
pub fn room_hash(event: &Event) -> u64 {
let mut hasher = DefaultHasher::new();
let mut pubkeys: Vec<&PublicKey> = vec![];
// Add all public keys from event
pubkeys.push(&event.pubkey);
pubkeys.extend(
event
.tags
.public_keys()
.unique()
.sorted()
.collect::<Vec<_>>(),
);
// Generate unique hash
pubkeys
.into_iter()
.unique()
.sorted()
.collect::<Vec<_>>()
.hash(&mut hasher);
hasher.finish()
}
pub fn device_pubkey(event: &Event) -> Result<PublicKey, anyhow::Error> {
let n_tag = event.tags.find(TagKind::custom("n")).context("Invalid")?;
let hex = n_tag.content().context("Invalid")?;
let pubkey = PublicKey::parse(hex)?;
Ok(pubkey)
}
pub fn random_name(length: usize) -> String {
let rng = RNG::from(&Language::Roman);
rng.generate_names(length, true).join("-").to_lowercase()
}
pub fn create_qr(data: &str) -> Result<Arc<Image>, anyhow::Error> {
let qr = qrcode_generator::to_png_to_vec_from_str(data, QrCodeEcc::Medium, 256)?;
let img = Arc::new(Image {
format: gpui::ImageFormat::Png,
bytes: qr.clone(),
id: 1,
});
Ok(img)
}
pub fn compare<T>(a: &[T], b: &[T]) -> bool
where
T: Eq + Hash,
{
let a: HashSet<_> = a.iter().collect();
let b: HashSet<_> = b.iter().collect();
a == b
}

View File

@@ -19,9 +19,12 @@ pub fn get_client() -> &'static Client {
// Client options // Client options
let opts = Options::new() let opts = Options::new()
// NIP-65 // NIP-65
// Coop is don't really need to enable this option,
// but this will help the client discover user's messaging relays efficiently.
.gossip(true) .gossip(true)
// Skip all very slow relays // Skip all very slow relays
.max_avg_latency(Duration::from_secs(2)); // Note: max delay is 800ms
.max_avg_latency(Duration::from_millis(800));
// Setup Nostr Client // Setup Nostr Client
ClientBuilder::default().database(lmdb).opts(opts).build() ClientBuilder::default().database(lmdb).opts(opts).build()

View File

@@ -24,10 +24,3 @@ uuid = "1.10"
once_cell = "1.19.0" once_cell = "1.19.0"
image = "0.25.1" image = "0.25.1"
linkify = "0.10.0" linkify = "0.10.0"
[dev-dependencies]
criterion = "0.5"
[[bench]]
name = "text_benchmark"
harness = false

View File

@@ -1,142 +0,0 @@
use common::profile::NostrProfile;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use gpui::SharedString;
use nostr_sdk::prelude::*;
use ui::text::render_plain_text_mut;
fn create_test_profiles() -> Vec<NostrProfile> {
let mut profiles = Vec::new();
// Create a few test profiles
for i in 0..5 {
let keypair = Keys::generate();
let profile = NostrProfile {
public_key: keypair.public_key(),
name: SharedString::from(format!("user{}", i)),
avatar: SharedString::from(format!("avatar{}", i)),
// Add other required fields based on NostrProfile definition
// This is a simplified version - adjust based on your actual NostrProfile struct
};
profiles.push(profile);
}
profiles
}
fn benchmark_plain_text(c: &mut Criterion) {
let profiles = create_test_profiles();
// Simple text without any links or entities
let simple_text = "This is a simple text message without any links or entities.";
// Text with URLs
let text_with_urls =
"Check out https://example.com and https://nostr.com for more information.";
// Text with nostr entities
let text_with_nostr = "I found this note nostr:note1qw5uy7hsqs4jcsvmjc2rj5t6f5uuenwg3yapm5l58srprspvshlspr4mh3 from npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft";
// Mixed content with urls and nostr entities
let mixed_content = "Check out https://example.com and my profile nostr:npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft along with this event nevent1qw5uy7hsqs4jcsvmjc2rj5t6f5uuenwg3yapm5l58srprspvshlspr4mh3";
// Long text with multiple links and entities
let long_text = "Here's a long message with multiple links like https://example1.com, https://example2.com, and https://example3.com. It also has nostr entities like npub1l2vyh47mk2p0qlsku7hg0vn29faehy9hy34ygaclpn66ukqp3afqutajft, note1qw5uy7hsqs4jcsvmjc2rj5t6f5uuenwg3yapm5l58srprspvshlspr4mh3, and nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuerpd46hxtnfdupzp8xummjw3exgcnqvmpw35xjueqvdnyqystngfxk5hsnfd9h8jtr8a4klacnp".repeat(3);
// Benchmark with simple text
c.bench_function("render_plain_text_simple", |b| {
b.iter(|| {
let mut text = String::new();
let mut highlights = Vec::new();
let mut link_ranges = Vec::new();
let mut link_urls = Vec::new();
render_plain_text_mut(
black_box(simple_text),
black_box(&profiles),
&mut text,
&mut highlights,
&mut link_ranges,
&mut link_urls,
)
})
});
// Benchmark with URLs
c.bench_function("render_plain_text_urls", |b| {
b.iter(|| {
let mut text = String::new();
let mut highlights = Vec::new();
let mut link_ranges = Vec::new();
let mut link_urls = Vec::new();
render_plain_text_mut(
black_box(text_with_urls),
black_box(&profiles),
&mut text,
&mut highlights,
&mut link_ranges,
&mut link_urls,
)
})
});
// Benchmark with nostr entities
c.bench_function("render_plain_text_nostr", |b| {
b.iter(|| {
let mut text = String::new();
let mut highlights = Vec::new();
let mut link_ranges = Vec::new();
let mut link_urls = Vec::new();
render_plain_text_mut(
black_box(text_with_nostr),
black_box(&profiles),
&mut text,
&mut highlights,
&mut link_ranges,
&mut link_urls,
)
})
});
// Benchmark with mixed content
c.bench_function("render_plain_text_mixed", |b| {
b.iter(|| {
let mut text = String::new();
let mut highlights = Vec::new();
let mut link_ranges = Vec::new();
let mut link_urls = Vec::new();
render_plain_text_mut(
black_box(mixed_content),
black_box(&profiles),
&mut text,
&mut highlights,
&mut link_ranges,
&mut link_urls,
)
})
});
// Benchmark with long text
c.bench_function("render_plain_text_long", |b| {
b.iter(|| {
let mut text = String::new();
let mut highlights = Vec::new();
let mut link_ranges = Vec::new();
let mut link_urls = Vec::new();
render_plain_text_mut(
black_box(&long_text),
black_box(&profiles),
&mut text,
&mut highlights,
&mut link_ranges,
&mut link_urls,
)
})
});
}
criterion_group!(benches, benchmark_plain_text);
criterion_main!(benches);

View File

@@ -161,7 +161,7 @@ impl Element for Switch {
if !self.disabled if !self.disabled
&& prev_checked && prev_checked
.borrow() .borrow()
.map_or(false, |prev| prev != checked) .is_some_and(|prev| prev != checked)
{ {
let dur = Duration::from_secs_f64(0.15); let dur = Duration::from_secs_f64(0.15);
cx.spawn(async move |cx| { cx.spawn(async move |cx| {

View File

@@ -1,4 +1,4 @@
use common::profile::NostrProfile; use common::profile::SharedProfile;
use gpui::{ use gpui::{
AnyElement, AnyView, App, ElementId, FontWeight, HighlightStyle, InteractiveText, IntoElement, AnyElement, AnyView, App, ElementId, FontWeight, HighlightStyle, InteractiveText, IntoElement,
SharedString, StyledText, UnderlineStyle, Window, SharedString, StyledText, UnderlineStyle, Window,
@@ -43,7 +43,7 @@ pub struct RichText {
} }
impl RichText { impl RichText {
pub fn new(content: String, profiles: &[NostrProfile]) -> Self { pub fn new(content: String, profiles: &[Profile]) -> Self {
let mut text = String::new(); let mut text = String::new();
let mut highlights = Vec::new(); let mut highlights = Vec::new();
let mut link_ranges = Vec::new(); let mut link_ranges = Vec::new();
@@ -154,7 +154,7 @@ impl RichText {
pub fn render_plain_text_mut( pub fn render_plain_text_mut(
content: &str, content: &str,
profiles: &[NostrProfile], profiles: &[Profile],
text: &mut String, text: &mut String,
highlights: &mut Vec<(Range<usize>, Highlight)>, highlights: &mut Vec<(Range<usize>, Highlight)>,
link_ranges: &mut Vec<Range<usize>>, link_ranges: &mut Vec<Range<usize>>,
@@ -164,9 +164,9 @@ pub fn render_plain_text_mut(
text.push_str(content); text.push_str(content);
// Create a profile lookup using PublicKey directly // Create a profile lookup using PublicKey directly
let profile_lookup: HashMap<&PublicKey, &NostrProfile> = profiles let profile_lookup: HashMap<PublicKey, Profile> = profiles
.iter() .iter()
.map(|profile| (&profile.public_key, profile)) .map(|profile| (profile.public_key(), profile.clone()))
.collect(); .collect();
// Process regular URLs using linkify // Process regular URLs using linkify
@@ -263,18 +263,18 @@ pub fn render_plain_text_mut(
let profile_match = if entity_without_prefix.starts_with("npub") { let profile_match = if entity_without_prefix.starts_with("npub") {
PublicKey::from_bech32(entity_without_prefix) PublicKey::from_bech32(entity_without_prefix)
.ok() .ok()
.and_then(|pubkey| profile_lookup.get(&pubkey).copied()) .and_then(|pubkey| profile_lookup.get(&pubkey).cloned())
} else if entity_without_prefix.starts_with("nprofile") { } else if entity_without_prefix.starts_with("nprofile") {
Nip19Profile::from_bech32(entity_without_prefix) Nip19Profile::from_bech32(entity_without_prefix)
.ok() .ok()
.and_then(|profile| profile_lookup.get(&profile.public_key).copied()) .and_then(|profile| profile_lookup.get(&profile.public_key).cloned())
} else { } else {
None None
}; };
if let Some(profile) = profile_match { if let Some(profile) = profile_match {
// Profile found - create a mention // Profile found - create a mention
let display_name = format!("@{}", profile.name); let display_name = format!("@{}", profile.shared_name());
// Replace mention with profile name // Replace mention with profile name
text.replace_range(range.clone(), &display_name); text.replace_range(range.clone(), &display_name);

View File

@@ -1,5 +1,5 @@
[toolchain] [toolchain]
channel = "1.85" channel = "1.86"
profile = "minimal" profile = "minimal"
components = ["rustfmt", "clippy"] components = ["rustfmt", "clippy"]
targets = [ targets = [