feat: Rich Text Rendering (#13)
* add text * fix avatar is not show * refactor chats * improve rich text * add benchmark for text * update
This commit is contained in:
291
Cargo.lock
generated
291
Cargo.lock
generated
@@ -97,6 +97,12 @@ 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"
|
||||||
@@ -828,6 +834,12 @@ 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"
|
||||||
@@ -944,10 +956,12 @@ dependencies = [
|
|||||||
"gpui",
|
"gpui",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"log",
|
"log",
|
||||||
|
"nostr",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"oneshot",
|
"oneshot",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
|
"ui",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -964,6 +978,33 @@ 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"
|
||||||
@@ -988,9 +1029,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap"
|
name = "clap"
|
||||||
version = "4.5.32"
|
version = "4.5.33"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83"
|
checksum = "e2c80cae4c3350dd8f1272c73e83baff9a6ba550b8bfbe651b3c45b78cd1751e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"clap_builder",
|
"clap_builder",
|
||||||
"clap_derive",
|
"clap_derive",
|
||||||
@@ -998,9 +1039,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clap_builder"
|
name = "clap_builder"
|
||||||
version = "4.5.32"
|
version = "4.5.33"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8"
|
checksum = "0123e386f691c90aa228219b5b1ee72d465e8e231c79e9c82324f016a62a741c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
@@ -1108,7 +1149,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
@@ -1199,7 +1240,6 @@ dependencies = [
|
|||||||
"global",
|
"global",
|
||||||
"gpui",
|
"gpui",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"keyring",
|
|
||||||
"log",
|
"log",
|
||||||
"nostr-connect",
|
"nostr-connect",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
@@ -1351,6 +1391,42 @@ 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"
|
||||||
@@ -1430,35 +1506,6 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
|
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dbus"
|
|
||||||
version = "0.9.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"libdbus-sys",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dbus-secret-service"
|
|
||||||
version = "4.0.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b42a16374481d92aed73ae45b1f120207d8e71d24fb89f357fadbd8f946fd84b"
|
|
||||||
dependencies = [
|
|
||||||
"aes",
|
|
||||||
"block-padding",
|
|
||||||
"cbc",
|
|
||||||
"dbus",
|
|
||||||
"futures-util",
|
|
||||||
"hkdf",
|
|
||||||
"num",
|
|
||||||
"once_cell",
|
|
||||||
"rand 0.8.5",
|
|
||||||
"sha2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
version = "0.99.19"
|
version = "0.99.19"
|
||||||
@@ -1475,7 +1522,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#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1592,9 +1639,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dwrote"
|
name = "dwrote"
|
||||||
version = "0.11.2"
|
version = "0.11.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "70182709525a3632b2ba96b6569225467b18ecb4a77f46d255f713a6bebf05fd"
|
checksum = "bfe1f192fcce01590bd8d839aca53ce0d11d803bf291b2a6c4ad925a8f0024be"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -2235,7 +2282,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
@@ -2323,7 +2370,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#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2435,6 +2482,12 @@ 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"
|
||||||
@@ -2546,7 +2599,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#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2562,7 +2615,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#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls",
|
"rustls",
|
||||||
"rustls-platform-verifier",
|
"rustls-platform-verifier",
|
||||||
@@ -2697,9 +2750,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_locid_transform_data"
|
name = "icu_locid_transform_data"
|
||||||
version = "1.5.0"
|
version = "1.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
|
checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_normalizer"
|
name = "icu_normalizer"
|
||||||
@@ -2721,9 +2774,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_normalizer_data"
|
name = "icu_normalizer_data"
|
||||||
version = "1.5.0"
|
version = "1.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
|
checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_properties"
|
name = "icu_properties"
|
||||||
@@ -2742,9 +2795,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_properties_data"
|
name = "icu_properties_data"
|
||||||
version = "1.5.0"
|
version = "1.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
|
checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_provider"
|
name = "icu_provider"
|
||||||
@@ -2908,6 +2961,17 @@ 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"
|
||||||
@@ -2924,6 +2988,15 @@ 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"
|
||||||
@@ -3010,18 +3083,6 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "keyring"
|
|
||||||
version = "4.0.0-rc.1"
|
|
||||||
source = "git+https://github.com/hwchen/keyring-rs#9d1b02ff4c9fd1ff125c71f252c14b9ed7313fcb"
|
|
||||||
dependencies = [
|
|
||||||
"byteorder",
|
|
||||||
"dbus-secret-service",
|
|
||||||
"log",
|
|
||||||
"security-framework",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "khronos-egl"
|
name = "khronos-egl"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
@@ -3069,15 +3130,6 @@ version = "0.2.171"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
|
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libdbus-sys"
|
|
||||||
version = "0.2.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72"
|
|
||||||
dependencies = [
|
|
||||||
"pkg-config",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libfuzzer-sys"
|
name = "libfuzzer-sys"
|
||||||
version = "0.4.9"
|
version = "0.4.9"
|
||||||
@@ -3114,6 +3166,15 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linkify"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1dfa36d52c581e9ec783a7ce2a5e0143da6237be5811a0b3153fedfdbe9f780"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.15"
|
version = "0.4.15"
|
||||||
@@ -3268,7 +3329,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "media"
|
name = "media"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen 0.70.1",
|
"bindgen 0.70.1",
|
||||||
@@ -3447,7 +3508,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#b513d781b55ec90b367ce58a0d91ead4e92242e7"
|
source = "git+https://github.com/rust-nostr/nostr#07306c9333c08bad5dc633fd97a59d26eb3d4443"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -3472,7 +3533,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#b513d781b55ec90b367ce58a0d91ead4e92242e7"
|
source = "git+https://github.com/rust-nostr/nostr#07306c9333c08bad5dc633fd97a59d26eb3d4443"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -3484,7 +3545,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#b513d781b55ec90b367ce58a0d91ead4e92242e7"
|
source = "git+https://github.com/rust-nostr/nostr#07306c9333c08bad5dc633fd97a59d26eb3d4443"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"flatbuffers",
|
"flatbuffers",
|
||||||
"lru",
|
"lru",
|
||||||
@@ -3495,7 +3556,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#b513d781b55ec90b367ce58a0d91ead4e92242e7"
|
source = "git+https://github.com/rust-nostr/nostr#07306c9333c08bad5dc633fd97a59d26eb3d4443"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"heed",
|
"heed",
|
||||||
@@ -3508,7 +3569,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#b513d781b55ec90b367ce58a0d91ead4e92242e7"
|
source = "git+https://github.com/rust-nostr/nostr#07306c9333c08bad5dc633fd97a59d26eb3d4443"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"async-wsocket",
|
"async-wsocket",
|
||||||
@@ -3525,7 +3586,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#b513d781b55ec90b367ce58a0d91ead4e92242e7"
|
source = "git+https://github.com/rust-nostr/nostr#07306c9333c08bad5dc633fd97a59d26eb3d4443"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -3926,6 +3987,12 @@ 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"
|
||||||
@@ -4165,6 +4232,34 @@ 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"
|
||||||
@@ -4317,9 +4412,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-xml"
|
name = "quick-xml"
|
||||||
version = "0.37.2"
|
version = "0.37.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003"
|
checksum = "bf763ab1c7a3aa408be466efc86efe35ed1bd3dd74173ed39d6b0d0a6f0ba148"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
@@ -4597,7 +4692,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "refineable"
|
name = "refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_refineable",
|
"derive_refineable",
|
||||||
]
|
]
|
||||||
@@ -4726,7 +4821,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#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -4947,9 +5042,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.103.0"
|
version = "0.103.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0aa4eeac2588ffff23e9d7a7e9b3f971c5fb5b7ebc9452745e0c232c64f83b2f"
|
checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-rs",
|
"aws-lc-rs",
|
||||||
"ring",
|
"ring",
|
||||||
@@ -5138,7 +5233,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#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5460,7 +5555,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#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
@@ -5830,6 +5925,16 @@ 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"
|
||||||
@@ -6127,9 +6232,13 @@ version = "0.0.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"common",
|
||||||
|
"criterion",
|
||||||
"gpui",
|
"gpui",
|
||||||
"image",
|
"image",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
|
"linkify",
|
||||||
|
"nostr-sdk",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"paste",
|
"paste",
|
||||||
"regex",
|
"regex",
|
||||||
@@ -6322,7 +6431,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#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-fs",
|
"async-fs",
|
||||||
@@ -6375,9 +6484,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "value-bag"
|
name = "value-bag"
|
||||||
version = "1.10.0"
|
version = "1.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2"
|
checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"value-bag-serde1",
|
"value-bag-serde1",
|
||||||
"value-bag-sval2",
|
"value-bag-sval2",
|
||||||
@@ -6385,9 +6494,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "value-bag-serde1"
|
name = "value-bag-serde1"
|
||||||
version = "1.10.0"
|
version = "1.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4bb773bd36fd59c7ca6e336c94454d9c66386416734817927ac93d81cb3c5b0b"
|
checksum = "35540706617d373b118d550d41f5dfe0b78a0c195dc13c6815e92e2638432306"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"erased-serde",
|
"erased-serde",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -6396,9 +6505,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "value-bag-sval2"
|
name = "value-bag-sval2"
|
||||||
version = "1.10.0"
|
version = "1.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "53a916a702cac43a88694c97657d449775667bcd14b70419441d05b7fea4a83a"
|
checksum = "6fe7e140a2658cc16f7ee7a86e413e803fc8f9b5127adc8755c19f9fefa63a52"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sval",
|
"sval",
|
||||||
"sval_buffer",
|
"sval_buffer",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ gpui = { git = "https://github.com/zed-industries/zed" }
|
|||||||
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
||||||
|
|
||||||
# Nostr
|
# Nostr
|
||||||
|
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-sdk = { git = "https://github.com/rust-nostr/nostr", features = [
|
nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [
|
||||||
@@ -39,7 +40,6 @@ anyhow = "1.0.44"
|
|||||||
smallvec = "1.14.0"
|
smallvec = "1.14.0"
|
||||||
rust-embed = "8.5.0"
|
rust-embed = "8.5.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
keyring = { git = "https://github.com/hwchen/keyring-rs" }
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
strip = true
|
strip = true
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.1 KiB |
BIN
assets/brand/avatar.png
Normal file
BIN
assets/brand/avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
@@ -30,7 +30,6 @@ log.workspace = true
|
|||||||
smallvec.workspace = true
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
oneshot.workspace = true
|
oneshot.workspace = true
|
||||||
keyring.workspace = true
|
|
||||||
|
|
||||||
rustls = "0.23.23"
|
rustls = "0.23.23"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use account::Account;
|
|||||||
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,
|
||||||
Context, Entity, InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, Styled,
|
Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription,
|
||||||
StyledImage, Subscription, Task, Window,
|
Task, Window,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
@@ -172,14 +172,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, profile| this.child(img(profile.avatar.clone()).size_5()),
|
||||||
this.child(
|
|
||||||
img(profile.avatar.clone())
|
|
||||||
.size_5()
|
|
||||||
.rounded_full()
|
|
||||||
.object_fit(ObjectFit::Cover),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.popup_menu(move |this, _, _cx| {
|
.popup_menu(move |this, _, _cx| {
|
||||||
this.menu(
|
this.menu(
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ enum Signal {
|
|||||||
fn main() {
|
fn main() {
|
||||||
// Enable logging
|
// Enable logging
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
// Fix crash on startup
|
||||||
|
_ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||||
|
|
||||||
let (event_tx, event_rx) = smol::channel::bounded::<Signal>(1024);
|
let (event_tx, event_rx) = smol::channel::bounded::<Signal>(1024);
|
||||||
let (batch_tx, batch_rx) = smol::channel::bounded::<Vec<PublicKey>>(100);
|
let (batch_tx, batch_rx) = smol::channel::bounded::<Vec<PublicKey>>(100);
|
||||||
@@ -55,10 +57,6 @@ fn main() {
|
|||||||
// Connect to default relays
|
// Connect to default relays
|
||||||
app.background_executor()
|
app.background_executor()
|
||||||
.spawn(async {
|
.spawn(async {
|
||||||
// Fix crash on startup
|
|
||||||
// TODO: why this is needed?
|
|
||||||
_ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
|
||||||
|
|
||||||
for relay in BOOTSTRAP_RELAYS.into_iter() {
|
for relay in BOOTSTRAP_RELAYS.into_iter() {
|
||||||
_ = client.add_relay(relay).await;
|
_ = client.add_relay(relay).await;
|
||||||
}
|
}
|
||||||
@@ -223,13 +221,15 @@ fn main() {
|
|||||||
let chats = cx.update(|_, cx| ChatRegistry::global(cx)).unwrap();
|
let chats = cx.update(|_, cx| ChatRegistry::global(cx)).unwrap();
|
||||||
|
|
||||||
while let Ok(signal) = event_rx.recv().await {
|
while let Ok(signal) = event_rx.recv().await {
|
||||||
cx.update(|_, cx| {
|
cx.update(|window, cx| {
|
||||||
match signal {
|
match signal {
|
||||||
Signal::Eose => {
|
Signal::Eose => {
|
||||||
chats.update(cx, |this, cx| this.load_chat_rooms(cx));
|
chats.update(cx, |this, cx| this.load_chat_rooms(window, cx));
|
||||||
}
|
}
|
||||||
Signal::Event(event) => {
|
Signal::Event(event) => {
|
||||||
chats.update(cx, |this, cx| this.push_message(event, cx));
|
chats.update(cx, |this, cx| {
|
||||||
|
this.push_message(event, window, cx)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,89 +1,36 @@
|
|||||||
use anyhow::anyhow;
|
use anyhow::{anyhow, Error};
|
||||||
use async_utility::task::spawn;
|
use async_utility::task::spawn;
|
||||||
use chats::{room::Room, ChatRegistry};
|
use chats::{message::RoomMessage, room::Room, ChatRegistry};
|
||||||
use common::{
|
use common::utils::nip96_upload;
|
||||||
last_seen::LastSeen,
|
|
||||||
profile::NostrProfile,
|
|
||||||
utils::{compare, nip96_upload},
|
|
||||||
};
|
|
||||||
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,
|
||||||
Context, Element, Entity, EventEmitter, Flatten, FocusHandle, Focusable, InteractiveElement,
|
Context, Element, Entity, EventEmitter, Flatten, FocusHandle, Focusable, InteractiveElement,
|
||||||
IntoElement, ListAlignment, ListState, ObjectFit, ParentElement, PathPromptOptions, Render,
|
IntoElement, ListAlignment, ListState, ObjectFit, ParentElement, PathPromptOptions, Render,
|
||||||
SharedString, StatefulInteractiveElement, Styled, StyledImage, Subscription, WeakEntity,
|
SharedString, StatefulInteractiveElement, Styled, StyledImage, Subscription, Window,
|
||||||
Window,
|
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
use smol::fs;
|
use smol::fs;
|
||||||
use std::sync::Arc;
|
use std::{collections::HashMap, sync::Arc};
|
||||||
use ui::{
|
use ui::{
|
||||||
button::{Button, ButtonRounded, ButtonVariants},
|
button::{Button, ButtonRounded, ButtonVariants},
|
||||||
dock_area::panel::{Panel, PanelEvent},
|
dock_area::panel::{Panel, PanelEvent},
|
||||||
input::{InputEvent, TextInput},
|
input::{InputEvent, TextInput},
|
||||||
notification::Notification,
|
notification::Notification,
|
||||||
popup_menu::PopupMenu,
|
popup_menu::PopupMenu,
|
||||||
|
text::RichText,
|
||||||
theme::{scale::ColorScaleStep, ActiveTheme},
|
theme::{scale::ColorScaleStep, ActiveTheme},
|
||||||
v_flex, ContextModal, Icon, IconName, Sizable, StyledExt,
|
v_flex, ContextModal, Icon, IconName, Sizable, StyledExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
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.";
|
||||||
const DESCRIPTION: &str =
|
|
||||||
"This conversation is private. Only members of this chat can see each other's messages.";
|
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(id: &u64, window: &mut Window, cx: &mut App) -> Result<Arc<Entity<Chat>>, Error> {
|
||||||
id: &u64,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Result<Arc<Entity<Chat>>, anyhow::Error> {
|
|
||||||
if let Some(room) = ChatRegistry::global(cx).read(cx).get(id, cx) {
|
if let Some(room) = ChatRegistry::global(cx).read(cx).get(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 is not exist"))
|
Err(anyhow!("Chat Room not found."))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq)]
|
|
||||||
struct ParsedMessage {
|
|
||||||
avatar: SharedString,
|
|
||||||
display_name: SharedString,
|
|
||||||
created_at: SharedString,
|
|
||||||
content: SharedString,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ParsedMessage {
|
|
||||||
pub fn new(profile: &NostrProfile, content: &str, created_at: Timestamp) -> Self {
|
|
||||||
let content = SharedString::new(content);
|
|
||||||
let created_at = LastSeen(created_at).human_readable();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
avatar: profile.avatar.clone(),
|
|
||||||
display_name: profile.name.clone(),
|
|
||||||
created_at,
|
|
||||||
content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq)]
|
|
||||||
enum Message {
|
|
||||||
User(Box<ParsedMessage>),
|
|
||||||
System(SharedString),
|
|
||||||
Placeholder,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Message {
|
|
||||||
pub fn new(message: ParsedMessage) -> Self {
|
|
||||||
Self::User(Box::new(message))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn system(content: SharedString) -> Self {
|
|
||||||
Self::System(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn placeholder() -> Self {
|
|
||||||
Self::Placeholder
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,28 +39,22 @@ pub struct Chat {
|
|||||||
id: SharedString,
|
id: SharedString,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
// Chat Room
|
// Chat Room
|
||||||
room: WeakEntity<Room>,
|
room: Entity<Room>,
|
||||||
messages: Entity<Vec<Message>>,
|
messages: Entity<Vec<RoomMessage>>,
|
||||||
seens: Entity<Vec<EventId>>,
|
text_data: HashMap<EventId, RichText>,
|
||||||
list_state: ListState,
|
list_state: ListState,
|
||||||
#[allow(dead_code)]
|
|
||||||
subscriptions: Vec<Subscription>,
|
|
||||||
// New Message
|
// New Message
|
||||||
input: Entity<TextInput>,
|
input: Entity<TextInput>,
|
||||||
// Media
|
// Media Attachment
|
||||||
attaches: Entity<Option<Vec<Url>>>,
|
attaches: Entity<Option<Vec<Url>>>,
|
||||||
is_uploading: bool,
|
is_uploading: bool,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
subscriptions: SmallVec<[Subscription; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Chat {
|
impl Chat {
|
||||||
pub fn new(
|
pub fn new(id: &u64, room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||||
id: &u64,
|
let messages = cx.new(|_| vec![RoomMessage::announcement()]);
|
||||||
room: WeakEntity<Room>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Entity<Self> {
|
|
||||||
let messages = cx.new(|_| vec![Message::placeholder()]);
|
|
||||||
let seens = cx.new(|_| vec![]);
|
|
||||||
let attaches = cx.new(|_| None);
|
let attaches = cx.new(|_| None);
|
||||||
let input = cx.new(|cx| {
|
let input = cx.new(|cx| {
|
||||||
TextInput::new(window, cx)
|
TextInput::new(window, cx)
|
||||||
@@ -123,7 +64,7 @@ impl Chat {
|
|||||||
});
|
});
|
||||||
|
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
let mut subscriptions = Vec::with_capacity(2);
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
subscriptions.push(cx.subscribe_in(
|
subscriptions.push(cx.subscribe_in(
|
||||||
&input,
|
&input,
|
||||||
@@ -135,15 +76,21 @@ impl Chat {
|
|||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
if let Some(room) = room.upgrade() {
|
subscriptions.push(cx.subscribe_in(
|
||||||
subscriptions.push(cx.subscribe_in(
|
&room,
|
||||||
&room,
|
window,
|
||||||
window,
|
move |this, _, event, _window, cx| {
|
||||||
move |this: &mut Self, _, event, window, cx| {
|
let old_len = this.messages.read(cx).len();
|
||||||
this.push_message(&event.event, window, cx);
|
let message = event.event.clone();
|
||||||
},
|
|
||||||
));
|
cx.update_entity(&this.messages, |this, cx| {
|
||||||
}
|
this.extend(vec![message]);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.list_state.splice(old_len..old_len, 1);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
// Initialize list state
|
// Initialize list state
|
||||||
// [item_count] always equal to 1 at the beginning
|
// [item_count] always equal to 1 at the beginning
|
||||||
@@ -161,9 +108,9 @@ impl Chat {
|
|||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
is_uploading: false,
|
is_uploading: false,
|
||||||
id: id.to_string().into(),
|
id: id.to_string().into(),
|
||||||
|
text_data: HashMap::new(),
|
||||||
room,
|
room,
|
||||||
messages,
|
messages,
|
||||||
seens,
|
|
||||||
list_state,
|
list_state,
|
||||||
input,
|
input,
|
||||||
attaches,
|
attaches,
|
||||||
@@ -171,40 +118,37 @@ impl Chat {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Verify messaging relays of all members
|
// Verify messaging relays of all members
|
||||||
this.verify_messaging_relays(cx);
|
this.verify_messaging_relays(window, cx);
|
||||||
|
|
||||||
// Load all messages from database
|
// Load all messages from database
|
||||||
this.load_messages(cx);
|
this.load_messages(window, cx);
|
||||||
|
|
||||||
this
|
this
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_messaging_relays(&self, cx: &mut Context<Self>) {
|
fn verify_messaging_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Some(model) = self.room.upgrade() else {
|
let room = self.room.read(cx);
|
||||||
return;
|
let task = room.messaging_relays(cx);
|
||||||
};
|
|
||||||
|
|
||||||
let room = model.read(cx);
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
let task = room.verify_inbox_relays(cx);
|
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
if let Ok(result) = task.await {
|
if let Ok(result) = task.await {
|
||||||
cx.update(|cx| {
|
cx.update(|_, cx| {
|
||||||
_ = 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 Ok(Some(member)) =
|
if let Some(profile) =
|
||||||
this.room.read_with(cx, |this, _| this.member(&item.0))
|
this.room.read_with(cx, |this, _| this.member(&item.0))
|
||||||
{
|
{
|
||||||
this.push_system_message(
|
this.push_system_message(
|
||||||
format!("{} {}", member.name, ALERT),
|
format!("{} {}", profile.name, ALERT),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
|
.ok();
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
}
|
}
|
||||||
@@ -212,19 +156,27 @@ impl Chat {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_messages(&self, cx: &mut Context<Self>) {
|
fn load_messages(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Some(model) = self.room.upgrade() else {
|
let room = self.room.read(cx);
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let room = model.read(cx);
|
|
||||||
let task = room.load_messages(cx);
|
let task = room.load_messages(cx);
|
||||||
|
|
||||||
cx.spawn(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| {
|
cx.update(|_, cx| {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.push_messages(events, cx);
|
let old_len = this.messages.read(cx).len();
|
||||||
|
let new_len = events.len();
|
||||||
|
|
||||||
|
// Extend the messages list with the new events
|
||||||
|
this.messages.update(cx, |this, cx| {
|
||||||
|
this.extend(events);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update list state with the new messages
|
||||||
|
this.list_state.splice(old_len..old_len, new_len);
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
})
|
})
|
||||||
@@ -236,7 +188,7 @@ impl Chat {
|
|||||||
|
|
||||||
fn push_system_message(&self, content: String, cx: &mut Context<Self>) {
|
fn push_system_message(&self, content: String, cx: &mut Context<Self>) {
|
||||||
let old_len = self.messages.read(cx).len();
|
let old_len = self.messages.read(cx).len();
|
||||||
let message = Message::system(content.into());
|
let message = RoomMessage::system(content.into());
|
||||||
|
|
||||||
cx.update_entity(&self.messages, |this, cx| {
|
cx.update_entity(&self.messages, |this, cx| {
|
||||||
this.extend(vec![message]);
|
this.extend(vec![message]);
|
||||||
@@ -246,89 +198,7 @@ impl Chat {
|
|||||||
self.list_state.splice(old_len..old_len, 1);
|
self.list_state.splice(old_len..old_len, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push_message(&mut self, event: &Event, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let Some(model) = self.room.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Prevent duplicate messages
|
|
||||||
if self.seens.read(cx).iter().any(|id| id == &event.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Add ID to seen list
|
|
||||||
self.seen(event.id, cx);
|
|
||||||
|
|
||||||
let old_len = self.messages.read(cx).len();
|
|
||||||
let room = model.read(cx);
|
|
||||||
|
|
||||||
let profile = room
|
|
||||||
.member(&event.pubkey)
|
|
||||||
.unwrap_or(NostrProfile::new(event.pubkey, Metadata::default()));
|
|
||||||
|
|
||||||
let message = Message::new(ParsedMessage::new(
|
|
||||||
&profile,
|
|
||||||
&event.content,
|
|
||||||
Timestamp::now(),
|
|
||||||
));
|
|
||||||
|
|
||||||
cx.update_entity(&self.messages, |this, cx| {
|
|
||||||
this.extend(vec![message]);
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
|
|
||||||
self.list_state.splice(old_len..old_len, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_messages(&self, events: Events, cx: &mut Context<Self>) {
|
|
||||||
let Some(model) = self.room.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let room = model.read(cx);
|
|
||||||
let pubkeys = room.public_keys();
|
|
||||||
let old_len = self.messages.read(cx).len();
|
|
||||||
|
|
||||||
let (messages, new_len) = {
|
|
||||||
let items: Vec<Message> = events
|
|
||||||
.into_iter()
|
|
||||||
.sorted_by_key(|ev| ev.created_at)
|
|
||||||
.filter_map(|ev| {
|
|
||||||
let mut other_pubkeys = ev.tags.public_keys().copied().collect::<Vec<_>>();
|
|
||||||
other_pubkeys.push(ev.pubkey);
|
|
||||||
|
|
||||||
if !compare(&other_pubkeys, &pubkeys) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
room.members
|
|
||||||
.iter()
|
|
||||||
.find(|m| m.public_key == ev.pubkey)
|
|
||||||
.map(|member| {
|
|
||||||
Message::new(ParsedMessage::new(member, &ev.content, ev.created_at))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Used for update list state
|
|
||||||
let new_len = items.len();
|
|
||||||
|
|
||||||
(items, new_len)
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.update_entity(&self.messages, |this, cx| {
|
|
||||||
this.extend(messages);
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
|
|
||||||
self.list_state.splice(old_len..old_len, new_len);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn send_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn send_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Some(model) = self.room.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get message
|
|
||||||
let mut content = self.input.read(cx).text().to_string();
|
let mut content = self.input.read(cx).text().to_string();
|
||||||
|
|
||||||
// Get all attaches and merge its with message
|
// Get all attaches and merge its with message
|
||||||
@@ -353,7 +223,7 @@ impl Chat {
|
|||||||
this.set_disabled(true, window, cx);
|
this.set_disabled(true, window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let room = model.read(cx);
|
let room = self.room.read(cx);
|
||||||
let task = room.send_message(content, cx);
|
let task = room.send_message(content, cx);
|
||||||
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
@@ -384,8 +254,6 @@ impl Chat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn upload_media(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn upload_media(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let window_handle = window.window_handle();
|
|
||||||
|
|
||||||
let paths = cx.prompt_for_paths(PathPromptOptions {
|
let paths = cx.prompt_for_paths(PathPromptOptions {
|
||||||
files: true,
|
files: true,
|
||||||
directories: false,
|
directories: false,
|
||||||
@@ -395,25 +263,24 @@ impl Chat {
|
|||||||
// Show loading spinner
|
// Show loading spinner
|
||||||
self.set_loading(true, cx);
|
self.set_loading(true, cx);
|
||||||
|
|
||||||
// TODO: support multiple upload
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
match Flatten::flatten(paths.await.map_err(|e| e.into())) {
|
match Flatten::flatten(paths.await.map_err(|e| e.into())) {
|
||||||
Ok(Some(mut paths)) => {
|
Ok(Some(mut paths)) => {
|
||||||
let path = paths.pop().unwrap();
|
let path = paths.pop().unwrap();
|
||||||
|
|
||||||
if let Ok(file_data) = fs::read(path).await {
|
if let Ok(file_data) = fs::read(path).await {
|
||||||
|
let client = get_client();
|
||||||
let (tx, rx) = oneshot::channel::<Url>();
|
let (tx, rx) = oneshot::channel::<Url>();
|
||||||
|
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
let client = get_client();
|
|
||||||
if let Ok(url) = nip96_upload(client, file_data).await {
|
if let Ok(url) = nip96_upload(client, file_data).await {
|
||||||
_ = tx.send(url);
|
_ = tx.send(url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Ok(url) = rx.await {
|
if let Ok(url) = rx.await {
|
||||||
_ = cx.update_window(window_handle, |_, _, cx| {
|
cx.update(|_, cx| {
|
||||||
_ = this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
// Stop loading spinner
|
// Stop loading spinner
|
||||||
this.set_loading(false, cx);
|
this.set_loading(false, cx);
|
||||||
|
|
||||||
@@ -425,8 +292,10 @@ impl Chat {
|
|||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
});
|
.ok();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -460,30 +329,32 @@ impl Chat {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn seen(&mut self, id: EventId, cx: &mut Context<Self>) {
|
|
||||||
self.seens.update(cx, |this, cx| {
|
|
||||||
this.push(id);
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_message(
|
fn render_message(
|
||||||
&self,
|
&mut self,
|
||||||
ix: usize,
|
ix: usize,
|
||||||
_window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
if let Some(message) = self.messages.read(cx).get(ix) {
|
const ROOM_DESCRIPTION: &str =
|
||||||
div()
|
"This conversation is private. Only members of this chat can see each other's messages.";
|
||||||
.group("")
|
|
||||||
.relative()
|
let message = self.messages.read(cx).get(ix).unwrap();
|
||||||
.flex()
|
let text_data = &mut self.text_data;
|
||||||
.gap_3()
|
|
||||||
.w_full()
|
div()
|
||||||
.p_2()
|
.group("")
|
||||||
.map(|this| match message {
|
.relative()
|
||||||
Message::User(item) => this
|
.flex()
|
||||||
.hover(|this| this.bg(cx.theme().accent.step(cx, ColorScaleStep::ONE)))
|
.gap_3()
|
||||||
|
.w_full()
|
||||||
|
.p_2()
|
||||||
|
.map(|this| match message {
|
||||||
|
RoomMessage::User(item) => {
|
||||||
|
let text = text_data
|
||||||
|
.entry(item.id)
|
||||||
|
.or_insert_with(|| RichText::new(item.content.to_owned(), &item.mentions));
|
||||||
|
|
||||||
|
this.hover(|this| this.bg(cx.theme().accent.step(cx, ColorScaleStep::ONE)))
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.absolute()
|
.absolute()
|
||||||
@@ -496,12 +367,7 @@ impl Chat {
|
|||||||
this.bg(cx.theme().accent.step(cx, ColorScaleStep::NINE))
|
this.bg(cx.theme().accent.step(cx, ColorScaleStep::NINE))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(img(item.author.avatar.clone()).size_8().flex_shrink_0())
|
||||||
img(item.avatar.clone())
|
|
||||||
.size_8()
|
|
||||||
.rounded_full()
|
|
||||||
.flex_shrink_0(),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
@@ -515,57 +381,60 @@ impl Chat {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.child(
|
.child(
|
||||||
div().font_semibold().child(item.display_name.clone()),
|
div().font_semibold().child(item.author.name.clone()),
|
||||||
)
|
)
|
||||||
.child(div().child(item.created_at.clone()).text_color(
|
.child(
|
||||||
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
|
div()
|
||||||
)),
|
.child(item.created_at.human_readable())
|
||||||
|
.text_color(
|
||||||
|
cx.theme()
|
||||||
|
.base
|
||||||
|
.step(cx, ColorScaleStep::ELEVEN),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.child(div().text_sm().child(item.content.clone())),
|
.child(div().text_sm().child(text.element(
|
||||||
),
|
"body".into(),
|
||||||
Message::System(content) => this
|
window,
|
||||||
.items_center()
|
cx,
|
||||||
.child(
|
))),
|
||||||
div()
|
|
||||||
.absolute()
|
|
||||||
.left_0()
|
|
||||||
.top_0()
|
|
||||||
.w(px(2.))
|
|
||||||
.h_full()
|
|
||||||
.bg(cx.theme().transparent)
|
|
||||||
.group_hover("", |this| this.bg(cx.theme().danger)),
|
|
||||||
)
|
)
|
||||||
.child(
|
}
|
||||||
img("brand/avatar.jpg")
|
RoomMessage::System(content) => this
|
||||||
.size_8()
|
.items_center()
|
||||||
.rounded_full()
|
.child(
|
||||||
.flex_shrink_0(),
|
div()
|
||||||
)
|
.absolute()
|
||||||
.text_xs()
|
.left_0()
|
||||||
.text_color(cx.theme().danger)
|
.top_0()
|
||||||
.child(content.clone()),
|
.w(px(2.))
|
||||||
Message::Placeholder => this
|
.h_full()
|
||||||
.w_full()
|
.bg(cx.theme().transparent)
|
||||||
.h_32()
|
.group_hover("", |this| this.bg(cx.theme().danger)),
|
||||||
.flex()
|
)
|
||||||
.flex_col()
|
.child(img("brand/avatar.png").size_8().flex_shrink_0())
|
||||||
.items_center()
|
.text_xs()
|
||||||
.justify_center()
|
.text_color(cx.theme().danger)
|
||||||
.text_center()
|
.child(content.clone()),
|
||||||
.text_xs()
|
RoomMessage::Announcement => this
|
||||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
.w_full()
|
||||||
.line_height(relative(1.2))
|
.h_32()
|
||||||
.child(
|
.flex()
|
||||||
svg()
|
.flex_col()
|
||||||
.path("brand/coop.svg")
|
.items_center()
|
||||||
.size_8()
|
.justify_center()
|
||||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)),
|
.text_center()
|
||||||
)
|
.text_xs()
|
||||||
.child(DESCRIPTION),
|
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
||||||
})
|
.line_height(relative(1.3))
|
||||||
} else {
|
.child(
|
||||||
div()
|
svg()
|
||||||
}
|
.path("brand/coop.svg")
|
||||||
|
.size_8()
|
||||||
|
.text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)),
|
||||||
|
)
|
||||||
|
.child(ROOM_DESCRIPTION),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,37 +444,32 @@ impl Panel for Chat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn title(&self, cx: &App) -> AnyElement {
|
fn title(&self, cx: &App) -> AnyElement {
|
||||||
self.room
|
self.room.read_with(cx, |this, _| {
|
||||||
.read_with(cx, |this, _| {
|
let facepill: Vec<SharedString> = this
|
||||||
let facepill: Vec<SharedString> = this
|
.members
|
||||||
.members
|
.iter()
|
||||||
.iter()
|
.map(|member| member.avatar.clone())
|
||||||
.map(|member| member.avatar.clone())
|
.collect();
|
||||||
.collect();
|
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.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, face)| {
|
||||||
div().when(ix > 0, |div| div.ml_neg_1()).child(
|
div()
|
||||||
img(face)
|
.when(ix > 0, |div| div.ml_neg_1())
|
||||||
.size_4()
|
.child(img(face).size_4())
|
||||||
.rounded_full()
|
})),
|
||||||
.object_fit(ObjectFit::Cover),
|
)
|
||||||
)
|
.when_some(this.name(), |this, name| this.child(name))
|
||||||
})),
|
.into_any()
|
||||||
)
|
})
|
||||||
.when_some(this.name(), |this, name| this.child(name))
|
|
||||||
.into_any()
|
|
||||||
})
|
|
||||||
.unwrap_or("Unnamed".into_any())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
||||||
|
|||||||
@@ -264,12 +264,7 @@ impl Render for NewAccount {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if self.avatar_input.read(cx).text().is_empty() {
|
if self.avatar_input.read(cx).text().is_empty() {
|
||||||
this.child(
|
this.child(img("brand/avatar.png").size_10().flex_shrink_0())
|
||||||
img("brand/avatar.jpg")
|
|
||||||
.size_10()
|
|
||||||
.rounded_full()
|
|
||||||
.flex_shrink_0(),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
this.child(
|
this.child(
|
||||||
img(format!(
|
img(format!(
|
||||||
@@ -278,7 +273,6 @@ impl Render for NewAccount {
|
|||||||
self.avatar_input.read(cx).text()
|
self.avatar_input.read(cx).text()
|
||||||
))
|
))
|
||||||
.size_10()
|
.size_10()
|
||||||
.rounded_full()
|
|
||||||
.flex_shrink_0(),
|
.flex_shrink_0(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -296,12 +296,7 @@ impl Render for Profile {
|
|||||||
let picture = self.avatar_input.read(cx).text();
|
let picture = self.avatar_input.read(cx).text();
|
||||||
|
|
||||||
if picture.is_empty() {
|
if picture.is_empty() {
|
||||||
this.child(
|
this.child(img("brand/avatar.png").size_10().flex_shrink_0())
|
||||||
img("brand/avatar.jpg")
|
|
||||||
.size_10()
|
|
||||||
.rounded_full()
|
|
||||||
.flex_shrink_0(),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
this.child(
|
this.child(
|
||||||
img(format!(
|
img(format!(
|
||||||
@@ -310,7 +305,6 @@ impl Render for Profile {
|
|||||||
self.avatar_input.read(cx).text()
|
self.avatar_input.read(cx).text()
|
||||||
))
|
))
|
||||||
.size_10()
|
.size_10()
|
||||||
.rounded_full()
|
|
||||||
.flex_shrink_0(),
|
.flex_shrink_0(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,12 +117,7 @@ impl Sidebar {
|
|||||||
this.flex()
|
this.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(
|
.child(img(member.avatar.clone()).size_6().flex_shrink_0())
|
||||||
img(member.avatar.clone())
|
|
||||||
.size_6()
|
|
||||||
.rounded_full()
|
|
||||||
.flex_shrink_0(),
|
|
||||||
)
|
|
||||||
.child(member.name.clone())
|
.child(member.name.clone())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ publish.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
global = { path = "../global" }
|
global = { path = "../global" }
|
||||||
|
ui = { path = "../ui" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
nostr.workspace = true
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ use std::cmp::Reverse;
|
|||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use common::{last_seen::LastSeen, utils::room_hash};
|
use common::{last_seen::LastSeen, utils::room_hash};
|
||||||
use global::get_client;
|
use global::get_client;
|
||||||
use gpui::{App, AppContext, Context, Entity, Global, Task, WeakEntity};
|
use gpui::{App, AppContext, Context, Entity, Global, Task, Window};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
use crate::room::{IncomingEvent, Room};
|
use crate::room::Room;
|
||||||
|
|
||||||
|
pub mod message;
|
||||||
pub mod room;
|
pub mod room;
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
@@ -44,7 +45,7 @@ impl ChatRegistry {
|
|||||||
self.rooms.iter().map(|room| room.read(cx).id).collect()
|
self.rooms.iter().map(|room| room.read(cx).id).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_chat_rooms(&mut self, cx: &mut Context<Self>) {
|
pub fn load_chat_rooms(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let client = get_client();
|
let client = get_client();
|
||||||
|
|
||||||
let task: Task<Result<Vec<Event>, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<Vec<Event>, Error>> = cx.background_spawn(async move {
|
||||||
@@ -73,9 +74,9 @@ impl ChatRegistry {
|
|||||||
Ok(result)
|
Ok(result)
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn(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| {
|
cx.update(|_, cx| {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
if !events.is_empty() {
|
if !events.is_empty() {
|
||||||
let current_ids = this.current_rooms_ids(cx);
|
let current_ids = this.current_rooms_ids(cx);
|
||||||
@@ -118,11 +119,11 @@ impl ChatRegistry {
|
|||||||
self.is_loading
|
self.is_loading
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, id: &u64, cx: &App) -> Option<WeakEntity<Room>> {
|
pub fn get(&self, id: &u64, cx: &App) -> Option<Entity<Room>> {
|
||||||
self.rooms
|
self.rooms
|
||||||
.iter()
|
.iter()
|
||||||
.find(|model| model.read(cx).id == *id)
|
.find(|model| model.read(cx).id == *id)
|
||||||
.map(|room| room.downgrade())
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_room(
|
pub fn push_room(
|
||||||
@@ -144,14 +145,13 @@ impl ChatRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_message(&mut self, event: Event, 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.last_seen = LastSeen(event.created_at);
|
this.set_last_seen(LastSeen(event.created_at), cx);
|
||||||
cx.emit(IncomingEvent { event });
|
this.emit_message(event, window, cx);
|
||||||
cx.notify();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re-sort rooms by last seen
|
// Re-sort rooms by last seen
|
||||||
|
|||||||
57
crates/chats/src/message.rs
Normal file
57
crates/chats/src/message.rs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
use common::{last_seen::LastSeen, profile::NostrProfile};
|
||||||
|
use gpui::SharedString;
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Message {
|
||||||
|
pub id: EventId,
|
||||||
|
pub content: String,
|
||||||
|
pub author: NostrProfile,
|
||||||
|
pub mentions: Vec<NostrProfile>,
|
||||||
|
pub created_at: LastSeen,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message {
|
||||||
|
pub fn new(
|
||||||
|
id: EventId,
|
||||||
|
content: String,
|
||||||
|
author: NostrProfile,
|
||||||
|
mentions: Vec<NostrProfile>,
|
||||||
|
created_at: Timestamp,
|
||||||
|
) -> Self {
|
||||||
|
let created_at = LastSeen(created_at);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
content,
|
||||||
|
author,
|
||||||
|
mentions,
|
||||||
|
created_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum RoomMessage {
|
||||||
|
/// User message
|
||||||
|
User(Box<Message>),
|
||||||
|
/// System message
|
||||||
|
System(SharedString),
|
||||||
|
/// Only use for UI purposes.
|
||||||
|
/// Placeholder will be used for display room announcement
|
||||||
|
Announcement,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RoomMessage {
|
||||||
|
pub fn user(message: Message) -> Self {
|
||||||
|
Self::User(Box::new(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn system(content: SharedString) -> Self {
|
||||||
|
Self::System(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn announcement() -> Self {
|
||||||
|
Self::Announcement
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,22 @@
|
|||||||
use std::collections::HashSet;
|
use std::{collections::HashSet, sync::Arc};
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
use common::{last_seen::LastSeen, profile::NostrProfile, utils::room_hash};
|
use common::{
|
||||||
|
last_seen::LastSeen,
|
||||||
|
profile::NostrProfile,
|
||||||
|
utils::{compare, room_hash},
|
||||||
|
};
|
||||||
use global::get_client;
|
use global::get_client;
|
||||||
use gpui::{App, AppContext, Entity, EventEmitter, SharedString, Task};
|
use gpui::{App, AppContext, Context, Entity, EventEmitter, SharedString, Task, Window};
|
||||||
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smallvec::{smallvec, SmallVec};
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
|
||||||
|
use crate::message::{Message, RoomMessage};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct IncomingEvent {
|
pub struct IncomingEvent {
|
||||||
pub event: Event,
|
pub event: RoomMessage,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Room {
|
pub struct Room {
|
||||||
@@ -18,7 +25,7 @@ pub struct Room {
|
|||||||
/// Subject of the room
|
/// Subject of the room
|
||||||
pub name: Option<SharedString>,
|
pub name: Option<SharedString>,
|
||||||
/// All members of the room
|
/// All members of the room
|
||||||
pub members: SmallVec<[NostrProfile; 2]>,
|
pub members: Arc<SmallVec<[NostrProfile; 2]>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<IncomingEvent> for Room {}
|
impl EventEmitter<IncomingEvent> for Room {}
|
||||||
@@ -44,18 +51,19 @@ impl Room {
|
|||||||
// Create a task for loading metadata
|
// Create a task for loading metadata
|
||||||
let load_metadata = Self::load_metadata(event, cx);
|
let load_metadata = Self::load_metadata(event, cx);
|
||||||
|
|
||||||
|
// Create a new GPUI's Entity
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
let this = Self {
|
let this = Self {
|
||||||
id,
|
id,
|
||||||
last_seen,
|
last_seen,
|
||||||
name,
|
name,
|
||||||
members: smallvec![],
|
members: Arc::new(smallvec![]),
|
||||||
};
|
};
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
if let Ok(profiles) = load_metadata.await {
|
if let Ok(profiles) = load_metadata.await {
|
||||||
_ = cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
_ = this.update(cx, |this: &mut Room, cx| {
|
this.update(cx, |this: &mut Room, cx| {
|
||||||
// Update the room's name if it's not already set
|
// Update the room's name if it's not already set
|
||||||
if this.name.is_none() {
|
if this.name.is_none() {
|
||||||
let mut name = profiles
|
let mut name = profiles
|
||||||
@@ -71,12 +79,16 @@ impl Room {
|
|||||||
|
|
||||||
this.name = Some(name.into())
|
this.name = Some(name.into())
|
||||||
};
|
};
|
||||||
// Update the room's members
|
|
||||||
this.members.extend(profiles);
|
let mut new_members = SmallVec::new();
|
||||||
|
new_members.extend(profiles);
|
||||||
|
this.members = Arc::new(new_members);
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
})
|
||||||
});
|
.ok();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
@@ -122,13 +134,19 @@ impl Room {
|
|||||||
self.last_seen
|
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
|
/// Get room's last seen as ago format
|
||||||
pub fn ago(&self) -> SharedString {
|
pub fn ago(&self) -> SharedString {
|
||||||
self.last_seen.ago()
|
self.last_seen.ago()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync inbox relays for all room's members
|
/// Verify messaging_relays for all room's members
|
||||||
pub fn verify_inbox_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 = self.public_keys();
|
||||||
|
|
||||||
@@ -157,8 +175,6 @@ impl Room {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Send message to all room's members
|
/// Send message to all room's members
|
||||||
///
|
|
||||||
/// NIP-4e: Message will be signed by the device signer
|
|
||||||
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.public_keys();
|
||||||
@@ -192,21 +208,149 @@ impl Room {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load metadata for all members
|
/// Load room messages
|
||||||
pub fn load_messages(&self, cx: &App) -> Task<Result<Events, 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 = self.public_keys();
|
||||||
|
let members = Arc::clone(&self.members);
|
||||||
|
|
||||||
let filter = Filter::new()
|
let filter = Filter::new()
|
||||||
.kind(Kind::PrivateDirectMessage)
|
.kind(Kind::PrivateDirectMessage)
|
||||||
.authors(pubkeys.iter().copied())
|
.authors(pubkeys.clone())
|
||||||
.pubkeys(pubkeys);
|
.pubkeys(pubkeys.clone());
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let query = client.database().query(filter).await?;
|
let mut messages = vec![];
|
||||||
Ok(query)
|
let parser = NostrParser::new();
|
||||||
|
|
||||||
|
// Get all events from database
|
||||||
|
let events = client
|
||||||
|
.database()
|
||||||
|
.query(filter)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.sorted_by_key(|ev| ev.created_at)
|
||||||
|
.filter(|ev| {
|
||||||
|
let mut other_pubkeys = ev.tags.public_keys().copied().collect::<Vec<_>>();
|
||||||
|
other_pubkeys.push(ev.pubkey);
|
||||||
|
// Check if the event is from a member of the room
|
||||||
|
compare(&other_pubkeys, &pubkeys)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for event in events.into_iter() {
|
||||||
|
let mut mentions = vec![];
|
||||||
|
let content = event.content.clone();
|
||||||
|
let tokens = parser.parse(&content);
|
||||||
|
|
||||||
|
let author = members
|
||||||
|
.iter()
|
||||||
|
.find(|profile| profile.public_key == event.pubkey)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| NostrProfile::new(event.pubkey, Metadata::default()));
|
||||||
|
|
||||||
|
let pubkey_tokens = tokens
|
||||||
|
.filter_map(|token| match token {
|
||||||
|
Token::Nostr(nip21) => match nip21 {
|
||||||
|
Nip21::Pubkey(pubkey) => Some(pubkey),
|
||||||
|
Nip21::Profile(profile) => Some(profile.public_key),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for pubkey in pubkey_tokens {
|
||||||
|
if let Some(profile) =
|
||||||
|
members.iter().find(|profile| profile.public_key == pubkey)
|
||||||
|
{
|
||||||
|
mentions.push(profile.clone());
|
||||||
|
} 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 room_message = RoomMessage::user(message);
|
||||||
|
|
||||||
|
messages.push(room_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(messages)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Emit message to GPUI
|
||||||
|
pub fn emit_message(&self, event: Event, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let client = get_client();
|
||||||
|
let members = Arc::clone(&self.members);
|
||||||
|
|
||||||
|
let task: Task<Result<RoomMessage, Error>> = cx.background_spawn(async move {
|
||||||
|
let parser = NostrParser::new();
|
||||||
|
let content = event.content.clone();
|
||||||
|
let tokens = parser.parse(&content);
|
||||||
|
let mut mentions = vec![];
|
||||||
|
|
||||||
|
let author = members
|
||||||
|
.iter()
|
||||||
|
.find(|profile| profile.public_key == event.pubkey)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| NostrProfile::new(event.pubkey, Metadata::default()));
|
||||||
|
|
||||||
|
let pubkey_tokens = tokens
|
||||||
|
.filter_map(|token| match token {
|
||||||
|
Token::Nostr(nip21) => match nip21 {
|
||||||
|
Nip21::Pubkey(pubkey) => Some(pubkey),
|
||||||
|
Nip21::Profile(profile) => Some(profile.public_key),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for pubkey in pubkey_tokens {
|
||||||
|
if let Some(profile) = members
|
||||||
|
.iter()
|
||||||
|
.find(|profile| profile.public_key == event.pubkey)
|
||||||
|
{
|
||||||
|
mentions.push(profile.clone());
|
||||||
|
} 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 room_message = RoomMessage::user(message);
|
||||||
|
|
||||||
|
Ok(room_message)
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
if let Ok(message) = task.await {
|
||||||
|
cx.update(|_, cx| {
|
||||||
|
this.update(cx, |_, cx| {
|
||||||
|
cx.emit(IncomingEvent { event: message });
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
/// Load metadata for all members
|
/// Load metadata for all members
|
||||||
fn load_metadata(event: &Event, cx: &App) -> Task<Result<Vec<NostrProfile>, Error>> {
|
fn load_metadata(event: &Event, cx: &App) -> Task<Result<Vec<NostrProfile>, Error>> {
|
||||||
let client = get_client();
|
let client = get_client();
|
||||||
@@ -219,18 +363,23 @@ impl Room {
|
|||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
let signer_pubkey = signer.get_public_key().await?;
|
let signer_pubkey = signer.get_public_key().await?;
|
||||||
let mut profiles = vec![];
|
let mut profiles = Vec::with_capacity(pubkeys.len());
|
||||||
|
|
||||||
for public_key in pubkeys.into_iter() {
|
for public_key in pubkeys.into_iter() {
|
||||||
if let Ok(result) = client.database().metadata(public_key).await {
|
let metadata = client
|
||||||
let metadata = result.unwrap_or_default();
|
.database()
|
||||||
let profile = NostrProfile::new(public_key, metadata);
|
.metadata(public_key)
|
||||||
|
.await?
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
if public_key == signer_pubkey {
|
// Convert metadata to profile
|
||||||
profiles.push(profile);
|
let profile = NostrProfile::new(public_key, metadata);
|
||||||
} else {
|
|
||||||
profiles.insert(0, profile);
|
if public_key == signer_pubkey {
|
||||||
}
|
// Room's owner always push to the end of the vector
|
||||||
|
profiles.push(profile);
|
||||||
|
} else {
|
||||||
|
profiles.insert(0, profile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ impl NostrProfile {
|
|||||||
)
|
)
|
||||||
.into()
|
.into()
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| "brand/avatar.jpg".into())
|
.unwrap_or_else(|| "brand/avatar.png".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_name(public_key: &PublicKey, metadata: &Metadata) -> SharedString {
|
fn extract_name(public_key: &PublicKey, metadata: &Metadata) -> SharedString {
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ edition.workspace = true
|
|||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
common = { path = "../common" }
|
||||||
|
|
||||||
|
nostr-sdk.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
@@ -20,3 +23,11 @@ unicode-segmentation = "1.12.0"
|
|||||||
uuid = "1.10"
|
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"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
criterion = "0.5"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "text_benchmark"
|
||||||
|
harness = false
|
||||||
|
|||||||
142
crates/ui/benches/text_benchmark.rs
Normal file
142
crates/ui/benches/text_benchmark.rs
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
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);
|
||||||
@@ -22,6 +22,7 @@ pub mod scroll;
|
|||||||
pub mod skeleton;
|
pub mod skeleton;
|
||||||
pub mod switch;
|
pub mod switch;
|
||||||
pub mod tab;
|
pub mod tab;
|
||||||
|
pub mod text;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
pub mod tooltip;
|
pub mod tooltip;
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
px, relative, App, Axis, Bounds, ContentMask, Corners, Edges, Element, ElementId, EntityId,
|
px, relative, App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId,
|
||||||
GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels, Point,
|
EntityId, GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels,
|
||||||
Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window,
|
Point, Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::AxisExt;
|
use crate::AxisExt;
|
||||||
@@ -111,6 +111,7 @@ impl Element for ScrollableMask {
|
|||||||
bounds,
|
bounds,
|
||||||
border_widths: Edges::all(px(1.0)),
|
border_widths: Edges::all(px(1.0)),
|
||||||
border_color: color,
|
border_color: color,
|
||||||
|
border_style: BorderStyle::Solid,
|
||||||
background: gpui::transparent_white().into(),
|
background: gpui::transparent_white().into(),
|
||||||
corner_radii: Corners::all(px(0.)),
|
corner_radii: Corners::all(px(0.)),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
fill, point, px, relative, App, Bounds, ContentMask, CursorStyle, Edges, Element, EntityId,
|
fill, point, px, relative, App, BorderStyle, Bounds, ContentMask, CursorStyle, Edges, Element,
|
||||||
Hitbox, Hsla, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels,
|
EntityId, Hitbox, Hsla, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||||
Point, Position, ScrollHandle, ScrollWheelEvent, UniformListScrollHandle, Window,
|
Pixels, Point, Position, ScrollHandle, ScrollWheelEvent, UniformListScrollHandle, Window,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
@@ -657,7 +657,7 @@ impl Element for Scrollbar {
|
|||||||
let margin_end = state.margin_end;
|
let margin_end = state.margin_end;
|
||||||
let is_vertical = axis.is_vertical();
|
let is_vertical = axis.is_vertical();
|
||||||
|
|
||||||
window.set_cursor_style(CursorStyle::default(), &state.bar_hitbox);
|
window.set_cursor_style(CursorStyle::default(), Some(&state.bar_hitbox));
|
||||||
|
|
||||||
window.paint_layer(hitbox_bounds, |cx| {
|
window.paint_layer(hitbox_bounds, |cx| {
|
||||||
cx.paint_quad(fill(state.bounds, state.bg));
|
cx.paint_quad(fill(state.bounds, state.bg));
|
||||||
@@ -682,6 +682,7 @@ impl Element for Scrollbar {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
border_color: state.border,
|
border_color: state.border,
|
||||||
|
border_style: BorderStyle::Solid,
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.paint_quad(
|
cx.paint_quad(
|
||||||
|
|||||||
379
crates/ui/src/text.rs
Normal file
379
crates/ui/src/text.rs
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
use common::profile::NostrProfile;
|
||||||
|
use gpui::{
|
||||||
|
AnyElement, AnyView, App, ElementId, FontWeight, HighlightStyle, InteractiveText, IntoElement,
|
||||||
|
SharedString, StyledText, UnderlineStyle, Window,
|
||||||
|
};
|
||||||
|
use linkify::{LinkFinder, LinkKind};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use regex::Regex;
|
||||||
|
use std::{collections::HashMap, ops::Range, sync::Arc};
|
||||||
|
|
||||||
|
use crate::theme::{scale::ColorScaleStep, ActiveTheme};
|
||||||
|
|
||||||
|
static NOSTR_URI_REGEX: Lazy<Regex> =
|
||||||
|
Lazy::new(|| Regex::new(r"nostr:(npub|note|nprofile|nevent|naddr)[a-zA-Z0-9]+").unwrap());
|
||||||
|
|
||||||
|
static BECH32_REGEX: Lazy<Regex> =
|
||||||
|
Lazy::new(|| Regex::new(r"\b(npub|note|nprofile|nevent|naddr)[a-zA-Z0-9]+\b").unwrap());
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum Highlight {
|
||||||
|
Highlight(HighlightStyle),
|
||||||
|
Mention,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HighlightStyle> for Highlight {
|
||||||
|
fn from(style: HighlightStyle) -> Self {
|
||||||
|
Self::Highlight(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomRangeTooltipFn =
|
||||||
|
Option<Arc<dyn Fn(usize, Range<usize>, &mut Window, &mut App) -> Option<AnyView>>>;
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct RichText {
|
||||||
|
pub text: SharedString,
|
||||||
|
pub highlights: Vec<(Range<usize>, Highlight)>,
|
||||||
|
pub link_ranges: Vec<Range<usize>>,
|
||||||
|
pub link_urls: Arc<[String]>,
|
||||||
|
pub custom_ranges: Vec<Range<usize>>,
|
||||||
|
custom_ranges_tooltip_fn: CustomRangeTooltipFn,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RichText {
|
||||||
|
pub fn new(content: String, profiles: &[NostrProfile]) -> Self {
|
||||||
|
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(
|
||||||
|
&content,
|
||||||
|
profiles,
|
||||||
|
&mut text,
|
||||||
|
&mut highlights,
|
||||||
|
&mut link_ranges,
|
||||||
|
&mut link_urls,
|
||||||
|
);
|
||||||
|
|
||||||
|
text.truncate(text.trim_end().len());
|
||||||
|
|
||||||
|
RichText {
|
||||||
|
text: SharedString::from(text),
|
||||||
|
link_urls: link_urls.into(),
|
||||||
|
link_ranges,
|
||||||
|
highlights,
|
||||||
|
custom_ranges: Vec::new(),
|
||||||
|
custom_ranges_tooltip_fn: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_tooltip_builder_for_custom_ranges(
|
||||||
|
&mut self,
|
||||||
|
f: impl Fn(usize, Range<usize>, &mut Window, &mut App) -> Option<AnyView> + 'static,
|
||||||
|
) {
|
||||||
|
self.custom_ranges_tooltip_fn = Some(Arc::new(f));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn element(&self, id: ElementId, window: &mut Window, cx: &App) -> AnyElement {
|
||||||
|
let link_color = cx.theme().accent.step(cx, ColorScaleStep::ELEVEN);
|
||||||
|
|
||||||
|
InteractiveText::new(
|
||||||
|
id,
|
||||||
|
StyledText::new(self.text.clone()).with_default_highlights(
|
||||||
|
&window.text_style(),
|
||||||
|
self.highlights.iter().map(|(range, highlight)| {
|
||||||
|
(
|
||||||
|
range.clone(),
|
||||||
|
match highlight {
|
||||||
|
Highlight::Highlight(highlight) => {
|
||||||
|
// Check if this is a link highlight by seeing if it has an underline
|
||||||
|
if highlight.underline.is_some() {
|
||||||
|
// It's a link, so apply the link color
|
||||||
|
let mut link_style = *highlight;
|
||||||
|
link_style.color = Some(link_color);
|
||||||
|
link_style
|
||||||
|
} else {
|
||||||
|
*highlight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Highlight::Mention => HighlightStyle {
|
||||||
|
color: Some(link_color),
|
||||||
|
font_weight: Some(FontWeight::MEDIUM),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.on_click(self.link_ranges.clone(), {
|
||||||
|
let link_urls = self.link_urls.clone();
|
||||||
|
move |ix, _, cx| {
|
||||||
|
let url = &link_urls[ix];
|
||||||
|
if url.starts_with("http") {
|
||||||
|
cx.open_url(url);
|
||||||
|
}
|
||||||
|
// Handle mention URLs
|
||||||
|
else if url.starts_with("mention:") {
|
||||||
|
// Handle mention clicks
|
||||||
|
// For example: cx.emit_custom_event(MentionClicked(url.strip_prefix("mention:").unwrap().to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.tooltip({
|
||||||
|
let link_ranges = self.link_ranges.clone();
|
||||||
|
let link_urls = self.link_urls.clone();
|
||||||
|
let custom_tooltip_ranges = self.custom_ranges.clone();
|
||||||
|
let custom_tooltip_fn = self.custom_ranges_tooltip_fn.clone();
|
||||||
|
move |idx, window, cx| {
|
||||||
|
for (ix, range) in link_ranges.iter().enumerate() {
|
||||||
|
if range.contains(&idx) {
|
||||||
|
let url = &link_urls[ix];
|
||||||
|
if url.starts_with("http") {
|
||||||
|
// return Some(LinkPreview::new(url, cx));
|
||||||
|
}
|
||||||
|
// You can add custom tooltip handling for mentions here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for range in &custom_tooltip_ranges {
|
||||||
|
if range.contains(&idx) {
|
||||||
|
if let Some(f) = &custom_tooltip_fn {
|
||||||
|
return f(idx, range.clone(), window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_plain_text_mut(
|
||||||
|
content: &str,
|
||||||
|
profiles: &[NostrProfile],
|
||||||
|
text: &mut String,
|
||||||
|
highlights: &mut Vec<(Range<usize>, Highlight)>,
|
||||||
|
link_ranges: &mut Vec<Range<usize>>,
|
||||||
|
link_urls: &mut Vec<String>,
|
||||||
|
) {
|
||||||
|
// Copy the content directly
|
||||||
|
text.push_str(content);
|
||||||
|
|
||||||
|
// Create a profile lookup using PublicKey directly
|
||||||
|
let profile_lookup: HashMap<&PublicKey, &NostrProfile> = profiles
|
||||||
|
.iter()
|
||||||
|
.map(|profile| (&profile.public_key, profile))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Process regular URLs using linkify
|
||||||
|
let mut finder = LinkFinder::new();
|
||||||
|
finder.kinds(&[LinkKind::Url]);
|
||||||
|
|
||||||
|
// Collect all URLs
|
||||||
|
let mut url_matches: Vec<(Range<usize>, String)> = Vec::new();
|
||||||
|
|
||||||
|
for link in finder.links(content) {
|
||||||
|
let start = link.start();
|
||||||
|
let end = link.end();
|
||||||
|
let range = start..end;
|
||||||
|
let url = link.as_str().to_string();
|
||||||
|
|
||||||
|
url_matches.push((range, url));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process nostr entities with nostr: prefix
|
||||||
|
let mut nostr_matches: Vec<(Range<usize>, String)> = Vec::new();
|
||||||
|
|
||||||
|
for nostr_match in NOSTR_URI_REGEX.find_iter(content) {
|
||||||
|
let start = nostr_match.start();
|
||||||
|
let end = nostr_match.end();
|
||||||
|
let range = start..end;
|
||||||
|
let nostr_uri = nostr_match.as_str().to_string();
|
||||||
|
|
||||||
|
// Check if this nostr URI overlaps with any already processed URL
|
||||||
|
if !url_matches
|
||||||
|
.iter()
|
||||||
|
.any(|(url_range, _)| url_range.start < range.end && range.start < url_range.end)
|
||||||
|
{
|
||||||
|
nostr_matches.push((range, nostr_uri));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process raw bech32 entities (without nostr: prefix)
|
||||||
|
let mut bech32_matches: Vec<(Range<usize>, String)> = Vec::new();
|
||||||
|
|
||||||
|
for bech32_match in BECH32_REGEX.find_iter(content) {
|
||||||
|
let start = bech32_match.start();
|
||||||
|
let end = bech32_match.end();
|
||||||
|
let range = start..end;
|
||||||
|
let bech32_entity = bech32_match.as_str().to_string();
|
||||||
|
|
||||||
|
// Check if this entity overlaps with any already processed matches
|
||||||
|
let overlaps_with_url = url_matches
|
||||||
|
.iter()
|
||||||
|
.any(|(url_range, _)| url_range.start < range.end && range.start < url_range.end);
|
||||||
|
|
||||||
|
let overlaps_with_nostr = nostr_matches
|
||||||
|
.iter()
|
||||||
|
.any(|(nostr_range, _)| nostr_range.start < range.end && range.start < nostr_range.end);
|
||||||
|
|
||||||
|
if !overlaps_with_url && !overlaps_with_nostr {
|
||||||
|
bech32_matches.push((range, bech32_entity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine all matches for processing from end to start
|
||||||
|
let mut all_matches = Vec::new();
|
||||||
|
all_matches.extend(url_matches);
|
||||||
|
all_matches.extend(nostr_matches);
|
||||||
|
all_matches.extend(bech32_matches);
|
||||||
|
|
||||||
|
// Sort by position (end to start) to avoid changing positions when replacing text
|
||||||
|
all_matches.sort_by(|(range_a, _), (range_b, _)| range_b.start.cmp(&range_a.start));
|
||||||
|
|
||||||
|
// Process all matches
|
||||||
|
for (range, entity) in all_matches {
|
||||||
|
if entity.starts_with("http") {
|
||||||
|
// Regular URL
|
||||||
|
highlights.push((
|
||||||
|
range.clone(),
|
||||||
|
Highlight::Highlight(HighlightStyle {
|
||||||
|
underline: Some(UnderlineStyle {
|
||||||
|
thickness: 1.0.into(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
|
||||||
|
link_ranges.push(range);
|
||||||
|
link_urls.push(entity);
|
||||||
|
} else {
|
||||||
|
let entity_without_prefix = if entity.starts_with("nostr:") {
|
||||||
|
entity.strip_prefix("nostr:").unwrap_or(&entity)
|
||||||
|
} else {
|
||||||
|
&entity
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try to find a matching profile if this is npub or nprofile
|
||||||
|
let profile_match = if entity_without_prefix.starts_with("npub") {
|
||||||
|
PublicKey::from_bech32(entity_without_prefix)
|
||||||
|
.ok()
|
||||||
|
.and_then(|pubkey| profile_lookup.get(&pubkey).copied())
|
||||||
|
} else if entity_without_prefix.starts_with("nprofile") {
|
||||||
|
Nip19Profile::from_bech32(entity_without_prefix)
|
||||||
|
.ok()
|
||||||
|
.and_then(|profile| profile_lookup.get(&profile.public_key).copied())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(profile) = profile_match {
|
||||||
|
// Profile found - create a mention
|
||||||
|
let display_name = format!("@{}", profile.name);
|
||||||
|
|
||||||
|
// Replace mention with profile name
|
||||||
|
text.replace_range(range.clone(), &display_name);
|
||||||
|
|
||||||
|
// Adjust ranges
|
||||||
|
let new_length = display_name.len();
|
||||||
|
let length_diff = new_length as isize - (range.end - range.start) as isize;
|
||||||
|
|
||||||
|
// New range for the replacement
|
||||||
|
let new_range = range.start..(range.start + new_length);
|
||||||
|
|
||||||
|
// Add highlight for the profile name
|
||||||
|
highlights.push((new_range.clone(), Highlight::Mention));
|
||||||
|
|
||||||
|
// Make it clickable
|
||||||
|
link_ranges.push(new_range);
|
||||||
|
link_urls.push(format!("mention:{}", entity_without_prefix));
|
||||||
|
|
||||||
|
// Adjust subsequent ranges if needed
|
||||||
|
if length_diff != 0 {
|
||||||
|
adjust_ranges(highlights, link_ranges, range.end, length_diff);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No profile match or not a profile entity - create njump.me link
|
||||||
|
let njump_url = format!("https://njump.me/{}", entity_without_prefix);
|
||||||
|
|
||||||
|
// Create a shortened display format for the URL
|
||||||
|
let shortened_entity = format_shortened_entity(entity_without_prefix);
|
||||||
|
let display_text = format!("https://njump.me/{}", shortened_entity);
|
||||||
|
|
||||||
|
// Replace the original entity with the shortened display version
|
||||||
|
text.replace_range(range.clone(), &display_text);
|
||||||
|
|
||||||
|
// Adjust the ranges
|
||||||
|
let new_length = display_text.len();
|
||||||
|
let length_diff = new_length as isize - (range.end - range.start) as isize;
|
||||||
|
|
||||||
|
// New range for the replacement
|
||||||
|
let new_range = range.start..(range.start + new_length);
|
||||||
|
|
||||||
|
// Add underline highlight
|
||||||
|
highlights.push((
|
||||||
|
new_range.clone(),
|
||||||
|
Highlight::Highlight(HighlightStyle {
|
||||||
|
underline: Some(UnderlineStyle {
|
||||||
|
thickness: 1.0.into(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Make it clickable
|
||||||
|
link_ranges.push(new_range);
|
||||||
|
link_urls.push(njump_url);
|
||||||
|
|
||||||
|
// Adjust subsequent ranges if needed
|
||||||
|
if length_diff != 0 {
|
||||||
|
adjust_ranges(highlights, link_ranges, range.end, length_diff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a bech32 entity with ellipsis and last 4 characters
|
||||||
|
fn format_shortened_entity(entity: &str) -> String {
|
||||||
|
let prefix_end = entity.find('1').unwrap_or(0);
|
||||||
|
|
||||||
|
if prefix_end > 0 && entity.len() > prefix_end + 5 {
|
||||||
|
let prefix = &entity[0..=prefix_end]; // Include the '1'
|
||||||
|
let suffix = &entity[entity.len() - 4..]; // Last 4 chars
|
||||||
|
|
||||||
|
format!("{}...{}", prefix, suffix)
|
||||||
|
} else {
|
||||||
|
entity.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to adjust ranges when text length changes
|
||||||
|
fn adjust_ranges(
|
||||||
|
highlights: &mut [(Range<usize>, Highlight)],
|
||||||
|
link_ranges: &mut [Range<usize>],
|
||||||
|
position: usize,
|
||||||
|
length_diff: isize,
|
||||||
|
) {
|
||||||
|
// Adjust highlight ranges
|
||||||
|
for (range, _) in highlights.iter_mut() {
|
||||||
|
if range.start > position {
|
||||||
|
range.start = (range.start as isize + length_diff) as usize;
|
||||||
|
range.end = (range.end as isize + length_diff) as usize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust link ranges
|
||||||
|
for range in link_ranges.iter_mut() {
|
||||||
|
if range.start > position {
|
||||||
|
range.start = (range.start as isize + length_diff) as usize;
|
||||||
|
range.end = (range.end as isize + length_diff) as usize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -104,7 +104,7 @@ impl RenderOnce for WindowBorder {
|
|||||||
CursorStyle::ResizeUpRightDownLeft
|
CursorStyle::ResizeUpRightDownLeft
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
&hitbox,
|
Some(&hitbox),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user