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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anes"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.18"
|
||||
@@ -828,6 +834,12 @@ dependencies = [
|
||||
"wayland-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cast"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
version = "0.1.2"
|
||||
@@ -944,10 +956,12 @@ dependencies = [
|
||||
"gpui",
|
||||
"itertools 0.13.0",
|
||||
"log",
|
||||
"nostr",
|
||||
"nostr-sdk",
|
||||
"oneshot",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"ui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -964,6 +978,33 @@ dependencies = [
|
||||
"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]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
@@ -988,9 +1029,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.32"
|
||||
version = "4.5.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83"
|
||||
checksum = "e2c80cae4c3350dd8f1272c73e83baff9a6ba550b8bfbe651b3c45b78cd1751e"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -998,9 +1039,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.32"
|
||||
version = "4.5.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8"
|
||||
checksum = "0123e386f691c90aa228219b5b1ee72d465e8e231c79e9c82324f016a62a741c"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -1108,7 +1149,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "collections"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
||||
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"rustc-hash 2.1.1",
|
||||
@@ -1199,7 +1240,6 @@ dependencies = [
|
||||
"global",
|
||||
"gpui",
|
||||
"itertools 0.13.0",
|
||||
"keyring",
|
||||
"log",
|
||||
"nostr-connect",
|
||||
"nostr-sdk",
|
||||
@@ -1351,6 +1391,42 @@ dependencies = [
|
||||
"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]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.6"
|
||||
@@ -1430,35 +1506,6 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "derive_more"
|
||||
version = "0.99.19"
|
||||
@@ -1475,7 +1522,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "derive_refineable"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
||||
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1592,9 +1639,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "dwrote"
|
||||
version = "0.11.2"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70182709525a3632b2ba96b6569225467b18ecb4a77f46d255f713a6bebf05fd"
|
||||
checksum = "bfe1f192fcce01590bd8d839aca53ce0d11d803bf291b2a6c4ad925a8f0024be"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
@@ -2235,7 +2282,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
||||
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"as-raw-xcb-connection",
|
||||
@@ -2323,7 +2370,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gpui_macros"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
||||
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2435,6 +2482,12 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
@@ -2546,7 +2599,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "http_client"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
||||
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -2562,7 +2615,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "http_client_tls"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
||||
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"rustls-platform-verifier",
|
||||
@@ -2697,9 +2750,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_locid_transform_data"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
|
||||
checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d"
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
@@ -2721,9 +2774,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
|
||||
checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
@@ -2742,9 +2795,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
|
||||
checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
@@ -2908,6 +2961,17 @@ dependencies = [
|
||||
"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]]
|
||||
name = "is-wsl"
|
||||
version = "0.4.0"
|
||||
@@ -2924,6 +2988,15 @@ version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
@@ -3010,18 +3083,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "khronos-egl"
|
||||
version = "6.0.0"
|
||||
@@ -3069,15 +3130,6 @@ version = "0.2.171"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "libfuzzer-sys"
|
||||
version = "0.4.9"
|
||||
@@ -3114,6 +3166,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linkify"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1dfa36d52c581e9ec783a7ce2a5e0143da6237be5811a0b3153fedfdbe9f780"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
@@ -3268,7 +3329,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "media"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
||||
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bindgen 0.70.1",
|
||||
@@ -3447,7 +3508,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
||||
[[package]]
|
||||
name = "nostr"
|
||||
version = "0.40.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#b513d781b55ec90b367ce58a0d91ead4e92242e7"
|
||||
source = "git+https://github.com/rust-nostr/nostr#07306c9333c08bad5dc633fd97a59d26eb3d4443"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"base64",
|
||||
@@ -3472,7 +3533,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-connect"
|
||||
version = "0.40.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#b513d781b55ec90b367ce58a0d91ead4e92242e7"
|
||||
source = "git+https://github.com/rust-nostr/nostr#07306c9333c08bad5dc633fd97a59d26eb3d4443"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"nostr",
|
||||
@@ -3484,7 +3545,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-database"
|
||||
version = "0.40.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#b513d781b55ec90b367ce58a0d91ead4e92242e7"
|
||||
source = "git+https://github.com/rust-nostr/nostr#07306c9333c08bad5dc633fd97a59d26eb3d4443"
|
||||
dependencies = [
|
||||
"flatbuffers",
|
||||
"lru",
|
||||
@@ -3495,7 +3556,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-lmdb"
|
||||
version = "0.40.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#b513d781b55ec90b367ce58a0d91ead4e92242e7"
|
||||
source = "git+https://github.com/rust-nostr/nostr#07306c9333c08bad5dc633fd97a59d26eb3d4443"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"heed",
|
||||
@@ -3508,7 +3569,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-relay-pool"
|
||||
version = "0.40.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#b513d781b55ec90b367ce58a0d91ead4e92242e7"
|
||||
source = "git+https://github.com/rust-nostr/nostr#07306c9333c08bad5dc633fd97a59d26eb3d4443"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"async-wsocket",
|
||||
@@ -3525,7 +3586,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "nostr-sdk"
|
||||
version = "0.40.0"
|
||||
source = "git+https://github.com/rust-nostr/nostr#b513d781b55ec90b367ce58a0d91ead4e92242e7"
|
||||
source = "git+https://github.com/rust-nostr/nostr#07306c9333c08bad5dc633fd97a59d26eb3d4443"
|
||||
dependencies = [
|
||||
"async-utility",
|
||||
"nostr",
|
||||
@@ -3926,6 +3987,12 @@ dependencies = [
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
version = "11.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.1"
|
||||
@@ -4165,6 +4232,34 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "png"
|
||||
version = "0.17.16"
|
||||
@@ -4317,9 +4412,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.37.2"
|
||||
version = "0.37.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003"
|
||||
checksum = "bf763ab1c7a3aa408be466efc86efe35ed1bd3dd74173ed39d6b0d0a6f0ba148"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -4597,7 +4692,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "refineable"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
||||
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||
dependencies = [
|
||||
"derive_refineable",
|
||||
]
|
||||
@@ -4726,7 +4821,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "reqwest_client"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
||||
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -4947,9 +5042,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.0"
|
||||
version = "0.103.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0aa4eeac2588ffff23e9d7a7e9b3f971c5fb5b7ebc9452745e0c232c64f83b2f"
|
||||
checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
@@ -5138,7 +5233,7 @@ checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe"
|
||||
[[package]]
|
||||
name = "semantic_version"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
||||
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -5460,7 +5555,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
[[package]]
|
||||
name = "sum_tree"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
||||
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"log",
|
||||
@@ -5830,6 +5925,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tinyvec"
|
||||
version = "1.9.0"
|
||||
@@ -6127,9 +6232,13 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"common",
|
||||
"criterion",
|
||||
"gpui",
|
||||
"image",
|
||||
"itertools 0.13.0",
|
||||
"linkify",
|
||||
"nostr-sdk",
|
||||
"once_cell",
|
||||
"paste",
|
||||
"regex",
|
||||
@@ -6322,7 +6431,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
[[package]]
|
||||
name = "util"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/zed-industries/zed#d9dcc5933449f37143fa38f8c91ee9b30960457b"
|
||||
source = "git+https://github.com/zed-industries/zed#47b94e5ef0efae21daa9799d59f4e133384c8019"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-fs",
|
||||
@@ -6375,9 +6484,9 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "value-bag"
|
||||
version = "1.10.0"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2"
|
||||
checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5"
|
||||
dependencies = [
|
||||
"value-bag-serde1",
|
||||
"value-bag-sval2",
|
||||
@@ -6385,9 +6494,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "value-bag-serde1"
|
||||
version = "1.10.0"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bb773bd36fd59c7ca6e336c94454d9c66386416734817927ac93d81cb3c5b0b"
|
||||
checksum = "35540706617d373b118d550d41f5dfe0b78a0c195dc13c6815e92e2638432306"
|
||||
dependencies = [
|
||||
"erased-serde",
|
||||
"serde",
|
||||
@@ -6396,9 +6505,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "value-bag-sval2"
|
||||
version = "1.10.0"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53a916a702cac43a88694c97657d449775667bcd14b70419441d05b7fea4a83a"
|
||||
checksum = "6fe7e140a2658cc16f7ee7a86e413e803fc8f9b5127adc8755c19f9fefa63a52"
|
||||
dependencies = [
|
||||
"sval",
|
||||
"sval_buffer",
|
||||
|
||||
@@ -15,6 +15,7 @@ gpui = { git = "https://github.com/zed-industries/zed" }
|
||||
reqwest_client = { git = "https://github.com/zed-industries/zed" }
|
||||
|
||||
# Nostr
|
||||
nostr = { git = "https://github.com/rust-nostr/nostr", features = ["parser"] }
|
||||
nostr-relay-builder = { 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 = [
|
||||
@@ -39,7 +40,6 @@ anyhow = "1.0.44"
|
||||
smallvec = "1.14.0"
|
||||
rust-embed = "8.5.0"
|
||||
log = "0.4"
|
||||
keyring = { git = "https://github.com/hwchen/keyring-rs" }
|
||||
|
||||
[profile.release]
|
||||
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
|
||||
smol.workspace = true
|
||||
oneshot.workspace = true
|
||||
keyring.workspace = true
|
||||
|
||||
rustls = "0.23.23"
|
||||
futures = "0.3"
|
||||
|
||||
@@ -2,8 +2,8 @@ use account::Account;
|
||||
use global::get_client;
|
||||
use gpui::{
|
||||
actions, div, img, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis,
|
||||
Context, Entity, InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, Styled,
|
||||
StyledImage, Subscription, Task, Window,
|
||||
Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription,
|
||||
Task, Window,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
@@ -172,14 +172,7 @@ impl ChatSpace {
|
||||
.icon(Icon::new(IconName::ChevronDownSmall))
|
||||
.when_some(
|
||||
Account::global(cx).read(cx).profile.as_ref(),
|
||||
|this, profile| {
|
||||
this.child(
|
||||
img(profile.avatar.clone())
|
||||
.size_5()
|
||||
.rounded_full()
|
||||
.object_fit(ObjectFit::Cover),
|
||||
)
|
||||
},
|
||||
|this, profile| this.child(img(profile.avatar.clone()).size_5()),
|
||||
)
|
||||
.popup_menu(move |this, _, _cx| {
|
||||
this.menu(
|
||||
|
||||
@@ -40,6 +40,8 @@ enum Signal {
|
||||
fn main() {
|
||||
// Enable logging
|
||||
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 (batch_tx, batch_rx) = smol::channel::bounded::<Vec<PublicKey>>(100);
|
||||
@@ -55,10 +57,6 @@ fn main() {
|
||||
// Connect to default relays
|
||||
app.background_executor()
|
||||
.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() {
|
||||
_ = client.add_relay(relay).await;
|
||||
}
|
||||
@@ -223,13 +221,15 @@ fn main() {
|
||||
let chats = cx.update(|_, cx| ChatRegistry::global(cx)).unwrap();
|
||||
|
||||
while let Ok(signal) = event_rx.recv().await {
|
||||
cx.update(|_, cx| {
|
||||
cx.update(|window, cx| {
|
||||
match signal {
|
||||
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) => {
|
||||
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 chats::{room::Room, ChatRegistry};
|
||||
use common::{
|
||||
last_seen::LastSeen,
|
||||
profile::NostrProfile,
|
||||
utils::{compare, nip96_upload},
|
||||
};
|
||||
use chats::{message::RoomMessage, room::Room, ChatRegistry};
|
||||
use common::utils::nip96_upload;
|
||||
use global::{constants::IMAGE_SERVICE, get_client};
|
||||
use gpui::{
|
||||
div, img, list, prelude::FluentBuilder, px, relative, svg, white, AnyElement, App, AppContext,
|
||||
Context, Element, Entity, EventEmitter, Flatten, FocusHandle, Focusable, InteractiveElement,
|
||||
IntoElement, ListAlignment, ListState, ObjectFit, ParentElement, PathPromptOptions, Render,
|
||||
SharedString, StatefulInteractiveElement, Styled, StyledImage, Subscription, WeakEntity,
|
||||
Window,
|
||||
SharedString, StatefulInteractiveElement, Styled, StyledImage, Subscription, Window,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use smol::fs;
|
||||
use std::sync::Arc;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use ui::{
|
||||
button::{Button, ButtonRounded, ButtonVariants},
|
||||
dock_area::panel::{Panel, PanelEvent},
|
||||
input::{InputEvent, TextInput},
|
||||
notification::Notification,
|
||||
popup_menu::PopupMenu,
|
||||
text::RichText,
|
||||
theme::{scale::ColorScaleStep, ActiveTheme},
|
||||
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 DESCRIPTION: &str =
|
||||
"This conversation is private. Only members of this chat can see each other's messages.";
|
||||
|
||||
pub fn init(
|
||||
id: &u64,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Result<Arc<Entity<Chat>>, anyhow::Error> {
|
||||
pub fn init(id: &u64, window: &mut Window, cx: &mut App) -> Result<Arc<Entity<Chat>>, Error> {
|
||||
if let Some(room) = ChatRegistry::global(cx).read(cx).get(id, cx) {
|
||||
Ok(Arc::new(Chat::new(id, room, window, cx)))
|
||||
} else {
|
||||
Err(anyhow!("Chat room is not exist"))
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
Err(anyhow!("Chat Room not found."))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,28 +39,22 @@ pub struct Chat {
|
||||
id: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
// Chat Room
|
||||
room: WeakEntity<Room>,
|
||||
messages: Entity<Vec<Message>>,
|
||||
seens: Entity<Vec<EventId>>,
|
||||
room: Entity<Room>,
|
||||
messages: Entity<Vec<RoomMessage>>,
|
||||
text_data: HashMap<EventId, RichText>,
|
||||
list_state: ListState,
|
||||
#[allow(dead_code)]
|
||||
subscriptions: Vec<Subscription>,
|
||||
// New Message
|
||||
input: Entity<TextInput>,
|
||||
// Media
|
||||
// Media Attachment
|
||||
attaches: Entity<Option<Vec<Url>>>,
|
||||
is_uploading: bool,
|
||||
#[allow(dead_code)]
|
||||
subscriptions: SmallVec<[Subscription; 2]>,
|
||||
}
|
||||
|
||||
impl Chat {
|
||||
pub fn new(
|
||||
id: &u64,
|
||||
room: WeakEntity<Room>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
let messages = cx.new(|_| vec![Message::placeholder()]);
|
||||
let seens = cx.new(|_| vec![]);
|
||||
pub fn new(id: &u64, room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
let messages = cx.new(|_| vec![RoomMessage::announcement()]);
|
||||
let attaches = cx.new(|_| None);
|
||||
let input = cx.new(|cx| {
|
||||
TextInput::new(window, cx)
|
||||
@@ -123,7 +64,7 @@ impl Chat {
|
||||
});
|
||||
|
||||
cx.new(|cx| {
|
||||
let mut subscriptions = Vec::with_capacity(2);
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
&input,
|
||||
@@ -135,15 +76,21 @@ impl Chat {
|
||||
},
|
||||
));
|
||||
|
||||
if let Some(room) = room.upgrade() {
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
&room,
|
||||
window,
|
||||
move |this: &mut Self, _, event, window, cx| {
|
||||
this.push_message(&event.event, window, cx);
|
||||
},
|
||||
));
|
||||
}
|
||||
subscriptions.push(cx.subscribe_in(
|
||||
&room,
|
||||
window,
|
||||
move |this, _, event, _window, cx| {
|
||||
let old_len = this.messages.read(cx).len();
|
||||
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
|
||||
// [item_count] always equal to 1 at the beginning
|
||||
@@ -161,9 +108,9 @@ impl Chat {
|
||||
focus_handle: cx.focus_handle(),
|
||||
is_uploading: false,
|
||||
id: id.to_string().into(),
|
||||
text_data: HashMap::new(),
|
||||
room,
|
||||
messages,
|
||||
seens,
|
||||
list_state,
|
||||
input,
|
||||
attaches,
|
||||
@@ -171,40 +118,37 @@ impl Chat {
|
||||
};
|
||||
|
||||
// Verify messaging relays of all members
|
||||
this.verify_messaging_relays(cx);
|
||||
this.verify_messaging_relays(window, cx);
|
||||
|
||||
// Load all messages from database
|
||||
this.load_messages(cx);
|
||||
this.load_messages(window, cx);
|
||||
|
||||
this
|
||||
})
|
||||
}
|
||||
|
||||
fn verify_messaging_relays(&self, cx: &mut Context<Self>) {
|
||||
let Some(model) = self.room.upgrade() else {
|
||||
return;
|
||||
};
|
||||
fn verify_messaging_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let room = self.room.read(cx);
|
||||
let task = room.messaging_relays(cx);
|
||||
|
||||
let room = model.read(cx);
|
||||
let task = room.verify_inbox_relays(cx);
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Ok(result) = task.await {
|
||||
cx.update(|cx| {
|
||||
_ = this.update(cx, |this, cx| {
|
||||
cx.update(|_, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
result.into_iter().for_each(|item| {
|
||||
if !item.1 {
|
||||
if let Ok(Some(member)) =
|
||||
if let Some(profile) =
|
||||
this.room.read_with(cx, |this, _| this.member(&item.0))
|
||||
{
|
||||
this.push_system_message(
|
||||
format!("{} {}", member.name, ALERT),
|
||||
format!("{} {}", profile.name, ALERT),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -212,19 +156,27 @@ impl Chat {
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn load_messages(&self, cx: &mut Context<Self>) {
|
||||
let Some(model) = self.room.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let room = model.read(cx);
|
||||
fn load_messages(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let room = self.room.read(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 {
|
||||
cx.update(|cx| {
|
||||
cx.update(|_, 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();
|
||||
})
|
||||
@@ -236,7 +188,7 @@ impl Chat {
|
||||
|
||||
fn push_system_message(&self, content: String, cx: &mut Context<Self>) {
|
||||
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| {
|
||||
this.extend(vec![message]);
|
||||
@@ -246,89 +198,7 @@ impl Chat {
|
||||
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>) {
|
||||
let Some(model) = self.room.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Get message
|
||||
let mut content = self.input.read(cx).text().to_string();
|
||||
|
||||
// Get all attaches and merge its with message
|
||||
@@ -353,7 +223,7 @@ impl Chat {
|
||||
this.set_disabled(true, window, cx);
|
||||
});
|
||||
|
||||
let room = model.read(cx);
|
||||
let room = self.room.read(cx);
|
||||
let task = room.send_message(content, 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>) {
|
||||
let window_handle = window.window_handle();
|
||||
|
||||
let paths = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: true,
|
||||
directories: false,
|
||||
@@ -395,25 +263,24 @@ impl Chat {
|
||||
// Show loading spinner
|
||||
self.set_loading(true, cx);
|
||||
|
||||
// TODO: support multiple upload
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
match Flatten::flatten(paths.await.map_err(|e| e.into())) {
|
||||
Ok(Some(mut paths)) => {
|
||||
let path = paths.pop().unwrap();
|
||||
|
||||
if let Ok(file_data) = fs::read(path).await {
|
||||
let client = get_client();
|
||||
let (tx, rx) = oneshot::channel::<Url>();
|
||||
|
||||
spawn(async move {
|
||||
let client = get_client();
|
||||
if let Ok(url) = nip96_upload(client, file_data).await {
|
||||
_ = tx.send(url);
|
||||
}
|
||||
});
|
||||
|
||||
if let Ok(url) = rx.await {
|
||||
_ = cx.update_window(window_handle, |_, _, cx| {
|
||||
_ = this.update(cx, |this, cx| {
|
||||
cx.update(|_, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
// Stop loading spinner
|
||||
this.set_loading(false, cx);
|
||||
|
||||
@@ -425,8 +292,10 @@ impl Chat {
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -460,30 +329,32 @@ impl Chat {
|
||||
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(
|
||||
&self,
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
if let Some(message) = self.messages.read(cx).get(ix) {
|
||||
div()
|
||||
.group("")
|
||||
.relative()
|
||||
.flex()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.p_2()
|
||||
.map(|this| match message {
|
||||
Message::User(item) => this
|
||||
.hover(|this| this.bg(cx.theme().accent.step(cx, ColorScaleStep::ONE)))
|
||||
const ROOM_DESCRIPTION: &str =
|
||||
"This conversation is private. Only members of this chat can see each other's messages.";
|
||||
|
||||
let message = self.messages.read(cx).get(ix).unwrap();
|
||||
let text_data = &mut self.text_data;
|
||||
|
||||
div()
|
||||
.group("")
|
||||
.relative()
|
||||
.flex()
|
||||
.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(
|
||||
div()
|
||||
.absolute()
|
||||
@@ -496,12 +367,7 @@ impl Chat {
|
||||
this.bg(cx.theme().accent.step(cx, ColorScaleStep::NINE))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
img(item.avatar.clone())
|
||||
.size_8()
|
||||
.rounded_full()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
.child(img(item.author.avatar.clone()).size_8().flex_shrink_0())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
@@ -515,57 +381,60 @@ impl Chat {
|
||||
.gap_2()
|
||||
.text_xs()
|
||||
.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(
|
||||
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
|
||||
)),
|
||||
.child(
|
||||
div()
|
||||
.child(item.created_at.human_readable())
|
||||
.text_color(
|
||||
cx.theme()
|
||||
.base
|
||||
.step(cx, ColorScaleStep::ELEVEN),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(div().text_sm().child(item.content.clone())),
|
||||
),
|
||||
Message::System(content) => this
|
||||
.items_center()
|
||||
.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(div().text_sm().child(text.element(
|
||||
"body".into(),
|
||||
window,
|
||||
cx,
|
||||
))),
|
||||
)
|
||||
.child(
|
||||
img("brand/avatar.jpg")
|
||||
.size_8()
|
||||
.rounded_full()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
.text_xs()
|
||||
.text_color(cx.theme().danger)
|
||||
.child(content.clone()),
|
||||
Message::Placeholder => this
|
||||
.w_full()
|
||||
.h_32()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_center()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
||||
.line_height(relative(1.2))
|
||||
.child(
|
||||
svg()
|
||||
.path("brand/coop.svg")
|
||||
.size_8()
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)),
|
||||
)
|
||||
.child(DESCRIPTION),
|
||||
})
|
||||
} else {
|
||||
div()
|
||||
}
|
||||
}
|
||||
RoomMessage::System(content) => this
|
||||
.items_center()
|
||||
.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.png").size_8().flex_shrink_0())
|
||||
.text_xs()
|
||||
.text_color(cx.theme().danger)
|
||||
.child(content.clone()),
|
||||
RoomMessage::Announcement => this
|
||||
.w_full()
|
||||
.h_32()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_center()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
||||
.line_height(relative(1.3))
|
||||
.child(
|
||||
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 {
|
||||
self.room
|
||||
.read_with(cx, |this, _| {
|
||||
let facepill: Vec<SharedString> = this
|
||||
.members
|
||||
.iter()
|
||||
.map(|member| member.avatar.clone())
|
||||
.collect();
|
||||
self.room.read_with(cx, |this, _| {
|
||||
let facepill: Vec<SharedString> = this
|
||||
.members
|
||||
.iter()
|
||||
.map(|member| member.avatar.clone())
|
||||
.collect();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row_reverse()
|
||||
.items_center()
|
||||
.justify_start()
|
||||
.children(facepill.into_iter().enumerate().rev().map(|(ix, face)| {
|
||||
div().when(ix > 0, |div| div.ml_neg_1()).child(
|
||||
img(face)
|
||||
.size_4()
|
||||
.rounded_full()
|
||||
.object_fit(ObjectFit::Cover),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.when_some(this.name(), |this, name| this.child(name))
|
||||
.into_any()
|
||||
})
|
||||
.unwrap_or("Unnamed".into_any())
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row_reverse()
|
||||
.items_center()
|
||||
.justify_start()
|
||||
.children(facepill.into_iter().enumerate().rev().map(|(ix, face)| {
|
||||
div()
|
||||
.when(ix > 0, |div| div.ml_neg_1())
|
||||
.child(img(face).size_4())
|
||||
})),
|
||||
)
|
||||
.when_some(this.name(), |this, name| this.child(name))
|
||||
.into_any()
|
||||
})
|
||||
}
|
||||
|
||||
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
||||
|
||||
@@ -264,12 +264,7 @@ impl Render for NewAccount {
|
||||
.gap_2()
|
||||
.map(|this| {
|
||||
if self.avatar_input.read(cx).text().is_empty() {
|
||||
this.child(
|
||||
img("brand/avatar.jpg")
|
||||
.size_10()
|
||||
.rounded_full()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
this.child(img("brand/avatar.png").size_10().flex_shrink_0())
|
||||
} else {
|
||||
this.child(
|
||||
img(format!(
|
||||
@@ -278,7 +273,6 @@ impl Render for NewAccount {
|
||||
self.avatar_input.read(cx).text()
|
||||
))
|
||||
.size_10()
|
||||
.rounded_full()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -296,12 +296,7 @@ impl Render for Profile {
|
||||
let picture = self.avatar_input.read(cx).text();
|
||||
|
||||
if picture.is_empty() {
|
||||
this.child(
|
||||
img("brand/avatar.jpg")
|
||||
.size_10()
|
||||
.rounded_full()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
this.child(img("brand/avatar.png").size_10().flex_shrink_0())
|
||||
} else {
|
||||
this.child(
|
||||
img(format!(
|
||||
@@ -310,7 +305,6 @@ impl Render for Profile {
|
||||
self.avatar_input.read(cx).text()
|
||||
))
|
||||
.size_10()
|
||||
.rounded_full()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -117,12 +117,7 @@ impl Sidebar {
|
||||
this.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
img(member.avatar.clone())
|
||||
.size_6()
|
||||
.rounded_full()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
.child(img(member.avatar.clone()).size_6().flex_shrink_0())
|
||||
.child(member.name.clone())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ publish.workspace = true
|
||||
[dependencies]
|
||||
common = { path = "../common" }
|
||||
global = { path = "../global" }
|
||||
ui = { path = "../ui" }
|
||||
|
||||
gpui.workspace = true
|
||||
nostr.workspace = true
|
||||
nostr-sdk.workspace = true
|
||||
anyhow.workspace = true
|
||||
itertools.workspace = true
|
||||
|
||||
@@ -3,12 +3,13 @@ use std::cmp::Reverse;
|
||||
use anyhow::anyhow;
|
||||
use common::{last_seen::LastSeen, utils::room_hash};
|
||||
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 nostr_sdk::prelude::*;
|
||||
|
||||
use crate::room::{IncomingEvent, Room};
|
||||
use crate::room::Room;
|
||||
|
||||
pub mod message;
|
||||
pub mod room;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
@@ -44,7 +45,7 @@ impl ChatRegistry {
|
||||
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 task: Task<Result<Vec<Event>, Error>> = cx.background_spawn(async move {
|
||||
@@ -73,9 +74,9 @@ impl ChatRegistry {
|
||||
Ok(result)
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Ok(events) = task.await {
|
||||
cx.update(|cx| {
|
||||
cx.update(|_, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
if !events.is_empty() {
|
||||
let current_ids = this.current_rooms_ids(cx);
|
||||
@@ -118,11 +119,11 @@ impl ChatRegistry {
|
||||
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
|
||||
.iter()
|
||||
.find(|model| model.read(cx).id == *id)
|
||||
.map(|room| room.downgrade())
|
||||
.cloned()
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if let Some(room) = self.rooms.iter().find(|room| room.read(cx).id == id) {
|
||||
room.update(cx, |this, cx| {
|
||||
this.last_seen = LastSeen(event.created_at);
|
||||
cx.emit(IncomingEvent { event });
|
||||
cx.notify();
|
||||
this.set_last_seen(LastSeen(event.created_at), cx);
|
||||
this.emit_message(event, window, cx);
|
||||
});
|
||||
|
||||
// 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 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 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 smallvec::{smallvec, SmallVec};
|
||||
|
||||
use crate::message::{Message, RoomMessage};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IncomingEvent {
|
||||
pub event: Event,
|
||||
pub event: RoomMessage,
|
||||
}
|
||||
|
||||
pub struct Room {
|
||||
@@ -18,7 +25,7 @@ pub struct Room {
|
||||
/// Subject of the room
|
||||
pub name: Option<SharedString>,
|
||||
/// All members of the room
|
||||
pub members: SmallVec<[NostrProfile; 2]>,
|
||||
pub members: Arc<SmallVec<[NostrProfile; 2]>>,
|
||||
}
|
||||
|
||||
impl EventEmitter<IncomingEvent> for Room {}
|
||||
@@ -44,18 +51,19 @@ impl Room {
|
||||
// Create a task for loading metadata
|
||||
let load_metadata = Self::load_metadata(event, cx);
|
||||
|
||||
// Create a new GPUI's Entity
|
||||
cx.new(|cx| {
|
||||
let this = Self {
|
||||
id,
|
||||
last_seen,
|
||||
name,
|
||||
members: smallvec![],
|
||||
members: Arc::new(smallvec![]),
|
||||
};
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
if let Ok(profiles) = load_metadata.await {
|
||||
_ = cx.update(|cx| {
|
||||
_ = this.update(cx, |this: &mut Room, cx| {
|
||||
cx.update(|cx| {
|
||||
this.update(cx, |this: &mut Room, cx| {
|
||||
// Update the room's name if it's not already set
|
||||
if this.name.is_none() {
|
||||
let mut name = profiles
|
||||
@@ -71,12 +79,16 @@ impl Room {
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
@@ -122,13 +134,19 @@ impl Room {
|
||||
self.last_seen
|
||||
}
|
||||
|
||||
/// Set room's last seen
|
||||
pub fn set_last_seen(&mut self, last_seen: LastSeen, cx: &mut Context<Self>) {
|
||||
self.last_seen = last_seen;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Get room's last seen as ago format
|
||||
pub fn ago(&self) -> SharedString {
|
||||
self.last_seen.ago()
|
||||
}
|
||||
|
||||
/// Sync inbox relays for all room's members
|
||||
pub fn verify_inbox_relays(&self, cx: &App) -> Task<Result<Vec<(PublicKey, bool)>, Error>> {
|
||||
/// Verify messaging_relays for all room's members
|
||||
pub fn messaging_relays(&self, cx: &App) -> Task<Result<Vec<(PublicKey, bool)>, Error>> {
|
||||
let client = get_client();
|
||||
let pubkeys = self.public_keys();
|
||||
|
||||
@@ -157,8 +175,6 @@ impl Room {
|
||||
}
|
||||
|
||||
/// 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>> {
|
||||
let client = get_client();
|
||||
let pubkeys = self.public_keys();
|
||||
@@ -192,21 +208,149 @@ impl Room {
|
||||
})
|
||||
}
|
||||
|
||||
/// Load metadata for all members
|
||||
pub fn load_messages(&self, cx: &App) -> Task<Result<Events, Error>> {
|
||||
/// Load room messages
|
||||
pub fn load_messages(&self, cx: &App) -> Task<Result<Vec<RoomMessage>, Error>> {
|
||||
let client = get_client();
|
||||
let pubkeys = self.public_keys();
|
||||
let members = Arc::clone(&self.members);
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::PrivateDirectMessage)
|
||||
.authors(pubkeys.iter().copied())
|
||||
.pubkeys(pubkeys);
|
||||
.authors(pubkeys.clone())
|
||||
.pubkeys(pubkeys.clone());
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let query = client.database().query(filter).await?;
|
||||
Ok(query)
|
||||
let mut messages = vec![];
|
||||
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
|
||||
fn load_metadata(event: &Event, cx: &App) -> Task<Result<Vec<NostrProfile>, Error>> {
|
||||
let client = get_client();
|
||||
@@ -219,18 +363,23 @@ impl Room {
|
||||
cx.background_spawn(async move {
|
||||
let signer = client.signer().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() {
|
||||
if let Ok(result) = client.database().metadata(public_key).await {
|
||||
let metadata = result.unwrap_or_default();
|
||||
let profile = NostrProfile::new(public_key, metadata);
|
||||
let metadata = client
|
||||
.database()
|
||||
.metadata(public_key)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
if public_key == signer_pubkey {
|
||||
profiles.push(profile);
|
||||
} else {
|
||||
profiles.insert(0, profile);
|
||||
}
|
||||
// Convert metadata to profile
|
||||
let profile = NostrProfile::new(public_key, metadata);
|
||||
|
||||
if public_key == signer_pubkey {
|
||||
// Room's owner always push to the end of the vector
|
||||
profiles.push(profile);
|
||||
} else {
|
||||
profiles.insert(0, profile);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ impl NostrProfile {
|
||||
)
|
||||
.into()
|
||||
})
|
||||
.unwrap_or_else(|| "brand/avatar.jpg".into())
|
||||
.unwrap_or_else(|| "brand/avatar.png".into())
|
||||
}
|
||||
|
||||
fn extract_name(public_key: &PublicKey, metadata: &Metadata) -> SharedString {
|
||||
|
||||
@@ -5,6 +5,9 @@ edition.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
common = { path = "../common" }
|
||||
|
||||
nostr-sdk.workspace = true
|
||||
gpui.workspace = true
|
||||
smol.workspace = true
|
||||
serde.workspace = true
|
||||
@@ -20,3 +23,11 @@ unicode-segmentation = "1.12.0"
|
||||
uuid = "1.10"
|
||||
once_cell = "1.19.0"
|
||||
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 switch;
|
||||
pub mod tab;
|
||||
pub mod text;
|
||||
pub mod theme;
|
||||
pub mod tooltip;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use gpui::{
|
||||
px, relative, App, Axis, Bounds, ContentMask, Corners, Edges, Element, ElementId, EntityId,
|
||||
GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels, Point,
|
||||
Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window,
|
||||
px, relative, App, Axis, BorderStyle, Bounds, ContentMask, Corners, Edges, Element, ElementId,
|
||||
EntityId, GlobalElementId, Hitbox, Hsla, IntoElement, IsZero as _, LayoutId, PaintQuad, Pixels,
|
||||
Point, Position, ScrollHandle, ScrollWheelEvent, Size, Style, Window,
|
||||
};
|
||||
|
||||
use crate::AxisExt;
|
||||
@@ -111,6 +111,7 @@ impl Element for ScrollableMask {
|
||||
bounds,
|
||||
border_widths: Edges::all(px(1.0)),
|
||||
border_color: color,
|
||||
border_style: BorderStyle::Solid,
|
||||
background: gpui::transparent_white().into(),
|
||||
corner_radii: Corners::all(px(0.)),
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use gpui::{
|
||||
fill, point, px, relative, App, Bounds, ContentMask, CursorStyle, Edges, Element, EntityId,
|
||||
Hitbox, Hsla, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels,
|
||||
Point, Position, ScrollHandle, ScrollWheelEvent, UniformListScrollHandle, Window,
|
||||
fill, point, px, relative, App, BorderStyle, Bounds, ContentMask, CursorStyle, Edges, Element,
|
||||
EntityId, Hitbox, Hsla, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||
Pixels, Point, Position, ScrollHandle, ScrollWheelEvent, UniformListScrollHandle, Window,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
@@ -657,7 +657,7 @@ impl Element for Scrollbar {
|
||||
let margin_end = state.margin_end;
|
||||
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| {
|
||||
cx.paint_quad(fill(state.bounds, state.bg));
|
||||
@@ -682,6 +682,7 @@ impl Element for Scrollbar {
|
||||
}
|
||||
},
|
||||
border_color: state.border,
|
||||
border_style: BorderStyle::Solid,
|
||||
});
|
||||
|
||||
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
|
||||
}
|
||||
},
|
||||
&hitbox,
|
||||
Some(&hitbox),
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user