feat: sharpen chat experiences (#9)
* feat: add global account and refactor chat registry * chore: improve last seen * chore: reduce string alloc * wip: refactor room * chore: fix edit profile panel * chore: refactor open window in main * chore: refactor sidebar * chore: refactor room
This commit is contained in:
137
Cargo.lock
generated
137
Cargo.lock
generated
@@ -2,6 +2,20 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "account"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"common",
|
||||||
|
"gpui",
|
||||||
|
"log",
|
||||||
|
"nostr-sdk",
|
||||||
|
"oneshot",
|
||||||
|
"smol",
|
||||||
|
"state",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
version = "0.24.2"
|
version = "0.24.2"
|
||||||
@@ -134,9 +148,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.95"
|
version = "1.0.96"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arbitrary"
|
name = "arbitrary"
|
||||||
@@ -445,9 +459,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-lc-rs"
|
name = "aws-lc-rs"
|
||||||
version = "1.12.2"
|
version = "1.12.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4c2b7ddaa2c56a367ad27a094ad8ef4faacf8a617c2575acb2ba88949df999ca"
|
checksum = "4cd755adf9707cf671e31d944a189be3deaaeee11c8bc1d669bb8022ac90fbd0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aws-lc-sys",
|
"aws-lc-sys",
|
||||||
"paste",
|
"paste",
|
||||||
@@ -456,9 +470,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-lc-sys"
|
name = "aws-lc-sys"
|
||||||
version = "0.25.1"
|
version = "0.26.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "54ac4f13dad353b209b34cbec082338202cbc01c8f00336b55c750c13ac91f8f"
|
checksum = "0f9dd2e03ee80ca2822dd6ea431163d2ef259f2066a4d6ccaca6d9dcb386aa43"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bindgen 0.69.5",
|
"bindgen 0.69.5",
|
||||||
"cc",
|
"cc",
|
||||||
@@ -844,9 +858,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.14"
|
version = "1.2.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0c3d1b2e905a3a7b00a6141adb0e4c0bb941d11caf55349d863942a1cc44e3c9"
|
checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jobserver",
|
"jobserver",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -931,6 +945,7 @@ dependencies = [
|
|||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"oneshot",
|
"oneshot",
|
||||||
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
"state",
|
"state",
|
||||||
]
|
]
|
||||||
@@ -1093,7 +1108,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "collections"
|
name = "collections"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
|
source = "git+https://github.com/zed-industries/zed#83513bab59c9b387b80211f44f392ae9dc2bcad7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
@@ -1174,6 +1189,7 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
|||||||
name = "coop"
|
name = "coop"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"account",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chats",
|
"chats",
|
||||||
"common",
|
"common",
|
||||||
@@ -1375,9 +1391,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctor"
|
name = "ctor"
|
||||||
version = "0.3.5"
|
version = "0.3.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cdd538fd2dbf2e5932fad5a4f983ff0458891f5ea40973fa2b7d3460ca378914"
|
checksum = "21d960ecacd0a1bf55e73144b72de745e7bf275c7952c50e36e8af0a0cb7ab1f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ctor-proc-macro",
|
"ctor-proc-macro",
|
||||||
]
|
]
|
||||||
@@ -1416,7 +1432,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#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
|
source = "git+https://github.com/zed-industries/zed#83513bab59c9b387b80211f44f392ae9dc2bcad7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2135,7 +2151,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui"
|
name = "gpui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
|
source = "git+https://github.com/zed-industries/zed#83513bab59c9b387b80211f44f392ae9dc2bcad7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
@@ -2222,7 +2238,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#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
|
source = "git+https://github.com/zed-industries/zed#83513bab59c9b387b80211f44f392ae9dc2bcad7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -2445,7 +2461,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#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
|
source = "git+https://github.com/zed-industries/zed#83513bab59c9b387b80211f44f392ae9dc2bcad7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2744,9 +2760,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inout"
|
name = "inout"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
|
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"block-padding",
|
"block-padding",
|
||||||
"generic-array",
|
"generic-array",
|
||||||
@@ -3019,9 +3035,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.25"
|
version = "0.4.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
|
checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"value-bag",
|
"value-bag",
|
||||||
@@ -3132,7 +3148,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "media"
|
name = "media"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
|
source = "git+https://github.com/zed-industries/zed#83513bab59c9b387b80211f44f392ae9dc2bcad7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bindgen 0.70.1",
|
"bindgen 0.70.1",
|
||||||
@@ -3206,9 +3222,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.4"
|
version = "0.8.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b"
|
checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"adler2",
|
"adler2",
|
||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
@@ -3311,7 +3327,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr"
|
name = "nostr"
|
||||||
version = "0.39.0"
|
version = "0.39.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#54dbd855c1143f77b5341fd522eb5bcedc5ed5c3"
|
source = "git+https://github.com/rust-nostr/nostr#75d56eebc45d2160c54b8d134e845708496d266c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"base64",
|
"base64",
|
||||||
@@ -3335,7 +3351,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-connect"
|
name = "nostr-connect"
|
||||||
version = "0.39.0"
|
version = "0.39.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#54dbd855c1143f77b5341fd522eb5bcedc5ed5c3"
|
source = "git+https://github.com/rust-nostr/nostr#75d56eebc45d2160c54b8d134e845708496d266c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -3347,7 +3363,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-database"
|
name = "nostr-database"
|
||||||
version = "0.39.0"
|
version = "0.39.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#54dbd855c1143f77b5341fd522eb5bcedc5ed5c3"
|
source = "git+https://github.com/rust-nostr/nostr#75d56eebc45d2160c54b8d134e845708496d266c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"flatbuffers",
|
"flatbuffers",
|
||||||
"lru",
|
"lru",
|
||||||
@@ -3358,7 +3374,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-lmdb"
|
name = "nostr-lmdb"
|
||||||
version = "0.39.0"
|
version = "0.39.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#54dbd855c1143f77b5341fd522eb5bcedc5ed5c3"
|
source = "git+https://github.com/rust-nostr/nostr#75d56eebc45d2160c54b8d134e845708496d266c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"heed",
|
"heed",
|
||||||
@@ -3371,7 +3387,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-relay-pool"
|
name = "nostr-relay-pool"
|
||||||
version = "0.39.0"
|
version = "0.39.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#54dbd855c1143f77b5341fd522eb5bcedc5ed5c3"
|
source = "git+https://github.com/rust-nostr/nostr#75d56eebc45d2160c54b8d134e845708496d266c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"async-wsocket",
|
"async-wsocket",
|
||||||
@@ -3388,7 +3404,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "nostr-sdk"
|
name = "nostr-sdk"
|
||||||
version = "0.39.0"
|
version = "0.39.0"
|
||||||
source = "git+https://github.com/rust-nostr/nostr#54dbd855c1143f77b5341fd522eb5bcedc5ed5c3"
|
source = "git+https://github.com/rust-nostr/nostr#75d56eebc45d2160c54b8d134e845708496d266c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-utility",
|
"async-utility",
|
||||||
"nostr",
|
"nostr",
|
||||||
@@ -3750,8 +3766,9 @@ checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oneshot"
|
name = "oneshot"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
source = "git+https://github.com/faern/oneshot#d36ef86c3cbcc54764391ae89805c160696cf57c"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4ce411919553d3f9fa53a0880544cda985a112117a0444d5ff1e870a893d6ea"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oo7"
|
name = "oo7"
|
||||||
@@ -4266,7 +4283,7 @@ checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha 0.9.0",
|
"rand_chacha 0.9.0",
|
||||||
"rand_core 0.9.1",
|
"rand_core 0.9.1",
|
||||||
"zerocopy 0.8.18",
|
"zerocopy 0.8.20",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4305,7 +4322,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a88e0da7a2c97baa202165137c158d0a2e824ac465d13d81046727b34cb247d3"
|
checksum = "a88e0da7a2c97baa202165137c158d0a2e824ac465d13d81046727b34cb247d3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.3.1",
|
"getrandom 0.3.1",
|
||||||
"zerocopy 0.8.18",
|
"zerocopy 0.8.20",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4431,9 +4448,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.8"
|
version = "0.5.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
|
checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.8.0",
|
"bitflags 2.8.0",
|
||||||
]
|
]
|
||||||
@@ -4452,7 +4469,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "refineable"
|
name = "refineable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/zed-industries/zed#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
|
source = "git+https://github.com/zed-industries/zed#83513bab59c9b387b80211f44f392ae9dc2bcad7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_refineable",
|
"derive_refineable",
|
||||||
]
|
]
|
||||||
@@ -4581,7 +4598,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#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
|
source = "git+https://github.com/zed-industries/zed#83513bab59c9b387b80211f44f392ae9dc2bcad7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -4620,9 +4637,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.9"
|
version = "0.17.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e75ec5e92c4d8aede845126adc388046234541629e76029599ed35a003c7ed24"
|
checksum = "d34b5020fcdea098ef7d95e9f89ec15952123a4a039badd09fabebe9e963e839"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
@@ -4961,7 +4978,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#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
|
source = "git+https://github.com/zed-industries/zed#83513bab59c9b387b80211f44f392ae9dc2bcad7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -4975,18 +4992,18 @@ checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.217"
|
version = "1.0.218"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
|
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.217"
|
version = "1.0.218"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -5015,9 +5032,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.138"
|
version = "1.0.139"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d434192e7da787e94a6ea7e9670b26a036d0ca41e0b7efb2676dd32bae872949"
|
checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"itoa",
|
"itoa",
|
||||||
@@ -5285,7 +5302,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#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
|
source = "git+https://github.com/zed-industries/zed#83513bab59c9b387b80211f44f392ae9dc2bcad7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayvec",
|
"arrayvec",
|
||||||
"log",
|
"log",
|
||||||
@@ -5978,9 +5995,9 @@ checksum = "1df77b101bcc4ea3d78dafc5ad7e4f58ceffe0b2b16bf446aeb50b6cb4157656"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.16"
|
version = "1.0.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034"
|
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-linebreak"
|
name = "unicode-linebreak"
|
||||||
@@ -6110,7 +6127,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#7a6b652ebcf9b7729b4ee69130bce0fbb529e6df"
|
source = "git+https://github.com/zed-industries/zed#83513bab59c9b387b80211f44f392ae9dc2bcad7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-fs",
|
"async-fs",
|
||||||
@@ -6135,9 +6152,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.13.2"
|
version = "1.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8c1f41ffb7cf259f1ecc2876861a17e7142e63ead296f671f81f6ae85903e0d6"
|
checksum = "93d59ca99a559661b96bf898d8fce28ed87935fd2bea9f05983c1464dd6c71b1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.3.1",
|
"getrandom 0.3.1",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -6221,9 +6238,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vswhom-sys"
|
name = "vswhom-sys"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18"
|
checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -6825,9 +6842,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "0.7.2"
|
version = "0.7.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603"
|
checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
@@ -7091,11 +7108,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.18"
|
version = "0.8.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "79386d31a42a4996e3336b0919ddb90f81112af416270cff95b5f5af22b839c2"
|
checksum = "dde3bb8c68a8f3f1ed4ac9221aad6b10cece3e60a8e2ea54a6a2dec806d0084c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerocopy-derive 0.8.18",
|
"zerocopy-derive 0.8.20",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7111,9 +7128,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy-derive"
|
name = "zerocopy-derive"
|
||||||
version = "0.8.18"
|
version = "0.8.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "76331675d372f91bf8d17e13afbd5fe639200b73d01f0fc748bb059f9cca2db7"
|
checksum = "eea57037071898bf96a6da35fd626f4f27e9cee3ead2a6c703cf09d472b2e700"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [
|
|||||||
] }
|
] }
|
||||||
|
|
||||||
smol = "2"
|
smol = "2"
|
||||||
oneshot = { git = "https://github.com/faern/oneshot" }
|
oneshot = "0.1.10"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
@@ -32,7 +32,7 @@ futures = "0.3.30"
|
|||||||
chrono = "0.4.38"
|
chrono = "0.4.38"
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
anyhow = "1.0.44"
|
anyhow = "1.0.44"
|
||||||
smallvec = "1.13.2"
|
smallvec = "1.14.0"
|
||||||
rust-embed = "8.5.0"
|
rust-embed = "8.5.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
|
||||||
|
|||||||
16
crates/account/Cargo.toml
Normal file
16
crates/account/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "account"
|
||||||
|
version = "0.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
common = { path = "../common" }
|
||||||
|
state = { path = "../state" }
|
||||||
|
|
||||||
|
gpui.workspace = true
|
||||||
|
nostr-sdk.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
oneshot.workspace = true
|
||||||
|
log.workspace = true
|
||||||
1
crates/account/src/lib.rs
Normal file
1
crates/account/src/lib.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub mod registry;
|
||||||
117
crates/account/src/registry.rs
Normal file
117
crates/account/src/registry.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
use anyhow::anyhow;
|
||||||
|
use common::{
|
||||||
|
constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID},
|
||||||
|
profile::NostrProfile,
|
||||||
|
};
|
||||||
|
use gpui::{App, AppContext, AsyncApp, Context, Entity, Global, Task};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use state::get_client;
|
||||||
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
struct GlobalAccount(Entity<Account>);
|
||||||
|
|
||||||
|
impl Global for GlobalAccount {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Account {
|
||||||
|
profile: NostrProfile,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Account {
|
||||||
|
pub fn global(cx: &App) -> Option<Entity<Self>> {
|
||||||
|
cx.try_global::<GlobalAccount>()
|
||||||
|
.map(|model| model.0.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_global(account: Entity<Self>, cx: &mut App) {
|
||||||
|
cx.set_global(GlobalAccount(account));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn login(signer: Arc<dyn NostrSigner>, cx: &AsyncApp) -> Task<Result<(), anyhow::Error>> {
|
||||||
|
let client = get_client();
|
||||||
|
let (tx, rx) = oneshot::channel::<Option<NostrProfile>>();
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
// Update nostr signer
|
||||||
|
_ = client.set_signer(signer).await;
|
||||||
|
// Verify nostr signer and get public key
|
||||||
|
let result = async {
|
||||||
|
let signer = client.signer().await?;
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
let metadata = client
|
||||||
|
.fetch_metadata(public_key, Duration::from_secs(2))
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
Ok::<_, anyhow::Error>(NostrProfile::new(public_key, metadata))
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
|
||||||
|
tx.send(result.ok()).ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
cx.spawn(|cx| async move {
|
||||||
|
if let Ok(Some(profile)) = rx.await {
|
||||||
|
cx.update(|cx| {
|
||||||
|
let this = cx.new(|cx| {
|
||||||
|
let this = Account { profile };
|
||||||
|
// Run initial sync data for this account
|
||||||
|
if let Some(task) = this.sync(cx) {
|
||||||
|
task.detach();
|
||||||
|
}
|
||||||
|
// Return
|
||||||
|
this
|
||||||
|
});
|
||||||
|
|
||||||
|
Self::set_global(this, cx)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("Login failed"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self) -> &NostrProfile {
|
||||||
|
&self.profile
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sync(&self, cx: &mut Context<Self>) -> Option<Task<()>> {
|
||||||
|
let client = get_client();
|
||||||
|
let public_key = self.profile.public_key();
|
||||||
|
|
||||||
|
let task = cx.background_spawn(async move {
|
||||||
|
// Set the default options for this task
|
||||||
|
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||||
|
|
||||||
|
// Create a filter to get contact list
|
||||||
|
let contact_list = Filter::new()
|
||||||
|
.kind(Kind::ContactList)
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if let Err(e) = client.subscribe(contact_list, Some(opts)).await {
|
||||||
|
log::error!("Failed to subscribe to contact list: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a filter for getting all gift wrapped events send to current user
|
||||||
|
let msg = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
||||||
|
let id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
|
||||||
|
|
||||||
|
if let Err(e) = client.subscribe_with_id(id, msg.clone(), Some(opts)).await {
|
||||||
|
log::error!("Failed to subscribe to all messages: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a filter to continuously receive new messages.
|
||||||
|
let new_msg = msg.limit(0);
|
||||||
|
let id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
||||||
|
|
||||||
|
if let Err(e) = client.subscribe_with_id(id, new_msg, None).await {
|
||||||
|
log::error!("Failed to subscribe to new messages: {}", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Some(task)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ ui = { path = "../ui" }
|
|||||||
common = { path = "../common" }
|
common = { path = "../common" }
|
||||||
state = { path = "../state" }
|
state = { path = "../state" }
|
||||||
chats = { path = "../chats" }
|
chats = { path = "../chats" }
|
||||||
|
account = { path = "../account" }
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
reqwest_client.workspace = true
|
reqwest_client.workspace = true
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use asset::Assets;
|
use asset::Assets;
|
||||||
use chats::registry::ChatRegistry;
|
use chats::registry::ChatRegistry;
|
||||||
use common::{
|
use common::constants::{
|
||||||
constants::{ALL_MESSAGES_SUB_ID, APP_ID, APP_NAME, KEYRING_SERVICE, NEW_MESSAGE_SUB_ID},
|
ALL_MESSAGES_SUB_ID, APP_ID, APP_NAME, KEYRING_SERVICE, NEW_MESSAGE_SUB_ID,
|
||||||
profile::NostrProfile,
|
|
||||||
};
|
};
|
||||||
use futures::{select, FutureExt};
|
use futures::{select, FutureExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@@ -14,16 +13,16 @@ use gpui::{point, SharedString, TitlebarOptions};
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use gpui::{WindowBackgroundAppearance, WindowDecorations};
|
use gpui::{WindowBackgroundAppearance, WindowDecorations};
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
|
use nostr_sdk::SubscriptionId;
|
||||||
use nostr_sdk::{
|
use nostr_sdk::{
|
||||||
pool::prelude::ReqExitPolicy, Client, Event, Filter, Keys, Kind, Metadata, PublicKey,
|
pool::prelude::ReqExitPolicy, Client, Event, Filter, Keys, Kind, PublicKey, RelayMessage,
|
||||||
RelayMessage, RelayPoolNotification, SubscribeAutoCloseOptions,
|
RelayPoolNotification, SubscribeAutoCloseOptions,
|
||||||
};
|
};
|
||||||
use nostr_sdk::{prelude::NostrEventsDatabaseExt, FromBech32, SubscriptionId};
|
|
||||||
use smol::Timer;
|
use smol::Timer;
|
||||||
use state::get_client;
|
use state::get_client;
|
||||||
use std::{collections::HashSet, mem, sync::Arc, time::Duration};
|
use std::{collections::HashSet, mem, sync::Arc, time::Duration};
|
||||||
use ui::{theme::Theme, Root};
|
use ui::{theme::Theme, Root};
|
||||||
use views::{app, onboarding, startup};
|
use views::{app, onboarding};
|
||||||
|
|
||||||
mod asset;
|
mod asset;
|
||||||
mod views;
|
mod views;
|
||||||
@@ -45,7 +44,7 @@ fn main() {
|
|||||||
// Enable logging
|
// Enable logging
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
let (event_tx, event_rx) = smol::channel::bounded::<Signal>(2048);
|
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);
|
||||||
|
|
||||||
// Initialize nostr client
|
// Initialize nostr client
|
||||||
@@ -176,31 +175,17 @@ fn main() {
|
|||||||
// Handle re-open window
|
// Handle re-open window
|
||||||
app.on_reopen(move |cx| {
|
app.on_reopen(move |cx| {
|
||||||
let client = get_client();
|
let client = get_client();
|
||||||
let (tx, rx) = oneshot::channel::<Option<NostrProfile>>();
|
let (tx, rx) = oneshot::channel::<bool>();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
if let Ok(signer) = client.signer().await {
|
let is_login = client.signer().await.is_ok();
|
||||||
if let Ok(public_key) = signer.get_public_key().await {
|
_ = tx.send(is_login);
|
||||||
let metadata =
|
|
||||||
if let Ok(Some(metadata)) = client.database().metadata(public_key).await {
|
|
||||||
metadata
|
|
||||||
} else {
|
|
||||||
Metadata::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
_ = tx.send(Some(NostrProfile::new(public_key, metadata)));
|
|
||||||
} else {
|
|
||||||
_ = tx.send(None);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_ = tx.send(None);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
if let Ok(result) = rx.await {
|
if let Ok(is_login) = rx.await {
|
||||||
_ = restore_window(result, &mut cx).await;
|
_ = restore_window(is_login, &mut cx).await;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
@@ -223,115 +208,90 @@ fn main() {
|
|||||||
items: vec![MenuItem::action("Quit", Quit)],
|
items: vec![MenuItem::action("Quit", Quit)],
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
// Open window with default options
|
// Spawn a task to handle events from nostr channel
|
||||||
cx.open_window(
|
cx.spawn(|cx| async move {
|
||||||
WindowOptions {
|
while let Ok(signal) = event_rx.recv().await {
|
||||||
#[cfg(not(target_os = "linux"))]
|
cx.update(|cx| {
|
||||||
titlebar: Some(TitlebarOptions {
|
if let Some(chats) = ChatRegistry::global(cx) {
|
||||||
title: Some(SharedString::new_static(APP_NAME)),
|
match signal {
|
||||||
traffic_light_position: Some(point(px(9.0), px(9.0))),
|
Signal::Eose => chats.update(cx, |this, cx| this.load_chat_rooms(cx)),
|
||||||
appears_transparent: true,
|
Signal::Event(event) => {
|
||||||
}),
|
chats.update(cx, |this, cx| this.push_message(event, cx))
|
||||||
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
|
}
|
||||||
None,
|
|
||||||
size(px(900.0), px(680.0)),
|
|
||||||
cx,
|
|
||||||
))),
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
window_background: WindowBackgroundAppearance::Transparent,
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
window_decorations: Some(WindowDecorations::Client),
|
|
||||||
kind: WindowKind::Normal,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
|window, cx| {
|
|
||||||
window.set_window_title(APP_NAME);
|
|
||||||
window.set_app_id(APP_ID);
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
window
|
|
||||||
.observe_window_appearance(|window, cx| {
|
|
||||||
Theme::sync_system_appearance(Some(window), cx);
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
let handle = window.window_handle();
|
|
||||||
let root = cx.new(|cx| Root::new(startup::init(window, cx).into(), window, cx));
|
|
||||||
|
|
||||||
let task = cx.read_credentials(KEYRING_SERVICE);
|
|
||||||
let (tx, rx) = oneshot::channel::<Option<NostrProfile>>();
|
|
||||||
|
|
||||||
// Read credential in OS Keyring
|
|
||||||
cx.background_spawn(async {
|
|
||||||
let profile = if let Ok(Some((npub, secret))) = task.await {
|
|
||||||
let public_key = PublicKey::from_bech32(&npub).unwrap();
|
|
||||||
let secret_hex = String::from_utf8(secret).unwrap();
|
|
||||||
let keys = Keys::parse(&secret_hex).unwrap();
|
|
||||||
|
|
||||||
// Update nostr signer
|
|
||||||
_ = client.set_signer(keys).await;
|
|
||||||
|
|
||||||
// Get user's metadata
|
|
||||||
let metadata = if let Ok(Some(metadata)) =
|
|
||||||
client.database().metadata(public_key).await
|
|
||||||
{
|
|
||||||
metadata
|
|
||||||
} else {
|
|
||||||
Metadata::new()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(NostrProfile::new(public_key, metadata))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
_ = tx.send(profile)
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
// Set root view based on credential status
|
|
||||||
cx.spawn(|mut cx| async move {
|
|
||||||
if let Ok(Some(profile)) = rx.await {
|
|
||||||
_ = cx.update_window(handle, |_, window, cx| {
|
|
||||||
window.replace_root(cx, |window, cx| {
|
|
||||||
Root::new(app::init(profile, window, cx).into(), window, cx)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_ = cx.update_window(handle, |_, window, cx| {
|
|
||||||
window.replace_root(cx, |window, cx| {
|
|
||||||
Root::new(onboarding::init(window, cx).into(), window, cx)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
cx.spawn(|cx| async move {
|
// Set up the window options
|
||||||
while let Ok(signal) = event_rx.recv().await {
|
let window_opts = WindowOptions {
|
||||||
cx.update(|cx| {
|
#[cfg(not(target_os = "linux"))]
|
||||||
match signal {
|
titlebar: Some(TitlebarOptions {
|
||||||
Signal::Eose => {
|
title: Some(SharedString::new_static(APP_NAME)),
|
||||||
if let Some(chats) = ChatRegistry::global(cx) {
|
traffic_light_position: Some(point(px(9.0), px(9.0))),
|
||||||
chats.update(cx, |this, cx| this.load_chat_rooms(cx))
|
appears_transparent: true,
|
||||||
}
|
}),
|
||||||
}
|
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
|
||||||
Signal::Event(event) => {
|
None,
|
||||||
if let Some(chats) = ChatRegistry::global(cx) {
|
size(px(900.0), px(680.0)),
|
||||||
chats.update(cx, |this, cx| this.push_message(event, cx))
|
cx,
|
||||||
}
|
))),
|
||||||
}
|
#[cfg(target_os = "linux")]
|
||||||
};
|
window_background: WindowBackgroundAppearance::Transparent,
|
||||||
})
|
#[cfg(target_os = "linux")]
|
||||||
.ok();
|
window_decorations: Some(WindowDecorations::Client),
|
||||||
}
|
kind: WindowKind::Normal,
|
||||||
})
|
..Default::default()
|
||||||
.detach();
|
};
|
||||||
|
|
||||||
root
|
// Create a task to read credentials from the keyring service
|
||||||
},
|
let task = cx.read_credentials(KEYRING_SERVICE);
|
||||||
)
|
let (tx, rx) = oneshot::channel::<bool>();
|
||||||
.expect("System error. Please re-open the app.");
|
|
||||||
|
// Read credential in OS Keyring
|
||||||
|
cx.background_spawn(async {
|
||||||
|
let is_ready = if let Ok(Some((_, secret))) = task.await {
|
||||||
|
let result = async {
|
||||||
|
let secret_hex = String::from_utf8(secret)?;
|
||||||
|
let keys = Keys::parse(&secret_hex)?;
|
||||||
|
|
||||||
|
// Update nostr signer
|
||||||
|
client.set_signer(keys).await;
|
||||||
|
|
||||||
|
Ok::<_, anyhow::Error>(true)
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
|
||||||
|
result.is_ok()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = tx.send(is_ready)
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
cx.spawn(|cx| async move {
|
||||||
|
if let Ok(is_ready) = rx.await {
|
||||||
|
if is_ready {
|
||||||
|
// Open a App window
|
||||||
|
cx.open_window(window_opts, |window, cx| {
|
||||||
|
cx.new(|cx| Root::new(app::init(window, cx).into(), window, cx))
|
||||||
|
})
|
||||||
|
.expect("Failed to open window");
|
||||||
|
} else {
|
||||||
|
// Open a Onboarding window
|
||||||
|
cx.open_window(window_opts, |window, cx| {
|
||||||
|
cx.new(|cx| Root::new(onboarding::init(window, cx).into(), window, cx))
|
||||||
|
})
|
||||||
|
.expect("Failed to open window");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,7 +307,7 @@ async fn sync_metadata(client: &Client, buffer: HashSet<PublicKey>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn restore_window(profile: Option<NostrProfile>, cx: &mut AsyncApp) -> anyhow::Result<()> {
|
async fn restore_window(is_login: bool, cx: &mut AsyncApp) -> anyhow::Result<()> {
|
||||||
let opts = cx
|
let opts = cx
|
||||||
.update(|cx| WindowOptions {
|
.update(|cx| WindowOptions {
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
@@ -370,7 +330,7 @@ async fn restore_window(profile: Option<NostrProfile>, cx: &mut AsyncApp) -> any
|
|||||||
})
|
})
|
||||||
.expect("Failed to set window options.");
|
.expect("Failed to set window options.");
|
||||||
|
|
||||||
if let Some(profile) = profile {
|
if is_login {
|
||||||
_ = cx.open_window(opts, |window, cx| {
|
_ = cx.open_window(opts, |window, cx| {
|
||||||
window.set_window_title(APP_NAME);
|
window.set_window_title(APP_NAME);
|
||||||
window.set_app_id(APP_ID);
|
window.set_app_id(APP_ID);
|
||||||
@@ -382,7 +342,7 @@ async fn restore_window(profile: Option<NostrProfile>, cx: &mut AsyncApp) -> any
|
|||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
cx.new(|cx| Root::new(app::init(profile, window, cx).into(), window, cx))
|
cx.new(|cx| Root::new(app::init(window, cx).into(), window, cx))
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
_ = cx.open_window(opts, |window, cx| {
|
_ = cx.open_window(opts, |window, cx| {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use common::profile::NostrProfile;
|
use account::registry::Account;
|
||||||
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, ObjectFit, ParentElement, Render, Styled,
|
||||||
@@ -38,21 +38,22 @@ impl AddPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dock actions
|
||||||
impl_internal_actions!(dock, [AddPanel]);
|
impl_internal_actions!(dock, [AddPanel]);
|
||||||
|
// Account actions
|
||||||
actions!(account, [Logout]);
|
actions!(account, [Logout]);
|
||||||
|
|
||||||
pub fn init(account: NostrProfile, window: &mut Window, cx: &mut App) -> Entity<AppView> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<AppView> {
|
||||||
AppView::new(account, window, cx)
|
AppView::new(window, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppView {
|
pub struct AppView {
|
||||||
account: NostrProfile,
|
|
||||||
relays: Entity<Option<Vec<String>>>,
|
relays: Entity<Option<Vec<String>>>,
|
||||||
dock: Entity<DockArea>,
|
dock: Entity<DockArea>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppView {
|
impl AppView {
|
||||||
pub fn new(account: NostrProfile, window: &mut Window, cx: &mut App) -> Entity<Self> {
|
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||||
// Initialize dock layout
|
// Initialize dock layout
|
||||||
let dock = cx.new(|cx| DockArea::new(window, cx));
|
let dock = cx.new(|cx| DockArea::new(window, cx));
|
||||||
let weak_dock = dock.downgrade();
|
let weak_dock = dock.downgrade();
|
||||||
@@ -83,76 +84,74 @@ impl AppView {
|
|||||||
});
|
});
|
||||||
|
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
let public_key = account.public_key();
|
|
||||||
let relays = cx.new(|_| None);
|
let relays = cx.new(|_| None);
|
||||||
let async_relays = relays.downgrade();
|
let this = Self { relays, dock };
|
||||||
|
|
||||||
// Check user's messaging relays and determine user is ready for NIP17 or not.
|
// Check user's messaging relays and determine user is ready for NIP17 or not.
|
||||||
// If not, show the setup modal and instruct user setup inbox relays
|
// If not, show the setup modal and instruct user setup inbox relays
|
||||||
let client = get_client();
|
this.verify_user_relays(window, cx);
|
||||||
let window_handle = window.window_handle();
|
|
||||||
let (tx, rx) = oneshot::channel::<Option<Vec<String>>>();
|
|
||||||
|
|
||||||
let this = Self {
|
|
||||||
account,
|
|
||||||
relays,
|
|
||||||
dock,
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let filter = Filter::new()
|
|
||||||
.kind(Kind::InboxRelays)
|
|
||||||
.author(public_key)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
let relays = if let Ok(events) = client.database().query(filter).await {
|
|
||||||
if let Some(event) = events.first_owned() {
|
|
||||||
Some(
|
|
||||||
event
|
|
||||||
.tags
|
|
||||||
.filter_standardized(TagKind::Relay)
|
|
||||||
.filter_map(|t| match t {
|
|
||||||
TagStandard::Relay(url) => Some(url.to_string()),
|
|
||||||
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
_ = tx.send(relays);
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
|
||||||
if let Ok(result) = rx.await {
|
|
||||||
if let Some(relays) = result {
|
|
||||||
_ = cx.update(|cx| {
|
|
||||||
_ = async_relays.update(cx, |this, cx| {
|
|
||||||
*this = Some(relays);
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
_ = cx.update_window(window_handle, |_, window, cx| {
|
|
||||||
this.update(cx, |this: &mut Self, cx| {
|
|
||||||
this.render_setup_relays(window, cx)
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
this
|
this
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn verify_user_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Some(account) = Account::global(cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let public_key = account.read(cx).get().public_key();
|
||||||
|
let client = get_client();
|
||||||
|
let window_handle = window.window_handle();
|
||||||
|
let (tx, rx) = oneshot::channel::<Option<Vec<String>>>();
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let filter = Filter::new()
|
||||||
|
.kind(Kind::InboxRelays)
|
||||||
|
.author(public_key)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
let relays = client
|
||||||
|
.database()
|
||||||
|
.query(filter)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.and_then(|events| events.first_owned())
|
||||||
|
.map(|event| {
|
||||||
|
event
|
||||||
|
.tags
|
||||||
|
.filter_standardized(TagKind::Relay)
|
||||||
|
.filter_map(|t| match t {
|
||||||
|
TagStandard::Relay(url) => Some(url.to_string()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
});
|
||||||
|
|
||||||
|
_ = tx.send(relays);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
if let Ok(Some(relays)) = rx.await {
|
||||||
|
_ = cx.update(|cx| {
|
||||||
|
_ = this.update(cx, |this, cx| {
|
||||||
|
let relays = cx.new(|_| Some(relays));
|
||||||
|
this.relays = relays;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
_ = cx.update_window(window_handle, |_, window, cx| {
|
||||||
|
this.update(cx, |this: &mut Self, cx| {
|
||||||
|
this.render_setup_relays(window, cx)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
fn render_setup_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
|
fn render_setup_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let relays = cx.new(|cx| Relays::new(None, window, cx));
|
let relays = cx.new(|cx| Relays::new(None, window, cx));
|
||||||
|
|
||||||
@@ -254,18 +253,22 @@ impl AppView {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_account(&self) -> impl IntoElement {
|
fn render_account(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
Button::new("account")
|
Button::new("account")
|
||||||
.ghost()
|
.ghost()
|
||||||
.xsmall()
|
.xsmall()
|
||||||
.reverse()
|
.reverse()
|
||||||
.icon(Icon::new(IconName::ChevronDownSmall))
|
.icon(Icon::new(IconName::ChevronDownSmall))
|
||||||
.child(
|
.when_some(Account::global(cx), |this, account| {
|
||||||
img(self.account.avatar())
|
let profile = account.read(cx).get();
|
||||||
.size_5()
|
|
||||||
.rounded_full()
|
this.child(
|
||||||
.object_fit(ObjectFit::Cover),
|
img(profile.avatar())
|
||||||
)
|
.size_5()
|
||||||
|
.rounded_full()
|
||||||
|
.object_fit(ObjectFit::Cover),
|
||||||
|
)
|
||||||
|
})
|
||||||
.popup_menu(move |this, _, _cx| {
|
.popup_menu(move |this, _, _cx| {
|
||||||
this.menu(
|
this.menu(
|
||||||
"Profile",
|
"Profile",
|
||||||
@@ -286,16 +289,19 @@ impl AppView {
|
|||||||
|
|
||||||
fn on_panel_action(&mut self, action: &AddPanel, window: &mut Window, cx: &mut Context<Self>) {
|
fn on_panel_action(&mut self, action: &AddPanel, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
match &action.panel {
|
match &action.panel {
|
||||||
PanelKind::Room(id) => match chat::init(id, window, cx) {
|
PanelKind::Room(id) => {
|
||||||
Ok(panel) => {
|
// User must be logged in to open a room
|
||||||
self.dock.update(cx, |dock_area, cx| {
|
match chat::init(id, window, cx) {
|
||||||
dock_area.add_panel(panel, action.position, window, cx);
|
Ok(panel) => {
|
||||||
});
|
self.dock.update(cx, |dock_area, cx| {
|
||||||
|
dock_area.add_panel(panel, action.position, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => window.push_notification(e.to_string(), cx),
|
||||||
}
|
}
|
||||||
Err(e) => window.push_notification(e.to_string(), cx),
|
}
|
||||||
},
|
|
||||||
PanelKind::Profile => {
|
PanelKind::Profile => {
|
||||||
let panel = Arc::new(profile::init(self.account.clone(), window, cx));
|
let panel = profile::init(window, cx);
|
||||||
|
|
||||||
self.dock.update(cx, |dock_area, cx| {
|
self.dock.update(cx, |dock_area, cx| {
|
||||||
dock_area.add_panel(panel, action.position, window, cx);
|
dock_area.add_panel(panel, action.position, window, cx);
|
||||||
@@ -319,8 +325,13 @@ impl AppView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_logout_action(&mut self, _action: &Logout, window: &mut Window, cx: &mut Context<Self>) {
|
fn on_logout_action(&mut self, _action: &Logout, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
cx.background_spawn(async move { get_client().reset().await })
|
let client = get_client();
|
||||||
.detach();
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
// Reset nostr client
|
||||||
|
client.reset().await
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
window.replace_root(cx, |window, cx| {
|
window.replace_root(cx, |window, cx| {
|
||||||
Root::new(onboarding::init(window, cx).into(), window, cx)
|
Root::new(onboarding::init(window, cx).into(), window, cx)
|
||||||
@@ -353,7 +364,7 @@ impl Render for AppView {
|
|||||||
.px_2()
|
.px_2()
|
||||||
.child(self.render_appearance_button(window, cx))
|
.child(self.render_appearance_button(window, cx))
|
||||||
.child(self.render_relays_button(window, cx))
|
.child(self.render_relays_button(window, cx))
|
||||||
.child(self.render_account()),
|
.child(self.render_account(cx)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(self.dock.clone())
|
.child(self.dock.clone())
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use account::registry::Account;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use async_utility::task::spawn;
|
use async_utility::task::spawn;
|
||||||
use chats::{registry::ChatRegistry, room::Room};
|
use chats::{registry::ChatRegistry, room::Room};
|
||||||
@@ -58,14 +59,12 @@ struct ParsedMessage {
|
|||||||
|
|
||||||
impl ParsedMessage {
|
impl ParsedMessage {
|
||||||
pub fn new(profile: &NostrProfile, content: &str, created_at: Timestamp) -> Self {
|
pub fn new(profile: &NostrProfile, content: &str, created_at: Timestamp) -> Self {
|
||||||
let avatar = profile.avatar().into();
|
|
||||||
let display_name = profile.name().into();
|
|
||||||
let content = SharedString::new(content);
|
let content = SharedString::new(content);
|
||||||
let created_at = LastSeen(created_at).human_readable();
|
let created_at = LastSeen(created_at).human_readable();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
avatar,
|
avatar: profile.avatar(),
|
||||||
display_name,
|
display_name: profile.name(),
|
||||||
created_at,
|
created_at,
|
||||||
content,
|
content,
|
||||||
}
|
}
|
||||||
@@ -96,13 +95,10 @@ impl Message {
|
|||||||
pub struct Chat {
|
pub struct Chat {
|
||||||
// Panel
|
// Panel
|
||||||
id: SharedString,
|
id: SharedString,
|
||||||
closable: bool,
|
|
||||||
zoomable: bool,
|
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
// Chat Room
|
// Chat Room
|
||||||
room: WeakEntity<Room>,
|
room: WeakEntity<Room>,
|
||||||
messages: Entity<Vec<Message>>,
|
messages: Entity<Vec<Message>>,
|
||||||
new_messages: Option<WeakEntity<Vec<Event>>>,
|
|
||||||
list_state: ListState,
|
list_state: ListState,
|
||||||
subscriptions: Vec<Subscription>,
|
subscriptions: Vec<Subscription>,
|
||||||
// New Message
|
// New Message
|
||||||
@@ -119,21 +115,16 @@ impl Chat {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Entity<Self> {
|
) -> Entity<Self> {
|
||||||
let new_messages = room
|
let messages = cx.new(|_| vec![Message::placeholder()]);
|
||||||
.read_with(cx, |this, _| this.new_messages.downgrade())
|
let attaches = cx.new(|_| None);
|
||||||
.ok();
|
let input = cx.new(|cx| {
|
||||||
|
TextInput::new(window, cx)
|
||||||
|
.appearance(false)
|
||||||
|
.text_size(ui::Size::Small)
|
||||||
|
.placeholder("Message...")
|
||||||
|
});
|
||||||
|
|
||||||
cx.new(|cx| {
|
cx.new(|cx| {
|
||||||
let messages = cx.new(|_| vec![Message::placeholder()]);
|
|
||||||
let attaches = cx.new(|_| None);
|
|
||||||
|
|
||||||
let input = cx.new(|cx| {
|
|
||||||
TextInput::new(window, cx)
|
|
||||||
.appearance(false)
|
|
||||||
.text_size(ui::Size::Small)
|
|
||||||
.placeholder("Message...")
|
|
||||||
});
|
|
||||||
|
|
||||||
let subscriptions = vec![cx.subscribe_in(
|
let subscriptions = vec![cx.subscribe_in(
|
||||||
&input,
|
&input,
|
||||||
window,
|
window,
|
||||||
@@ -157,13 +148,10 @@ impl Chat {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
closable: true,
|
|
||||||
zoomable: true,
|
|
||||||
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(),
|
||||||
room,
|
room,
|
||||||
new_messages,
|
|
||||||
messages,
|
messages,
|
||||||
list_state,
|
list_state,
|
||||||
input,
|
input,
|
||||||
@@ -189,11 +177,16 @@ impl Chat {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let room = model.read(cx);
|
|
||||||
let pubkeys: Vec<PublicKey> = room.members.iter().map(|m| m.public_key()).collect();
|
|
||||||
let client = get_client();
|
let client = get_client();
|
||||||
let (tx, rx) = oneshot::channel::<Vec<(PublicKey, bool)>>();
|
let (tx, rx) = oneshot::channel::<Vec<(PublicKey, bool)>>();
|
||||||
|
|
||||||
|
let pubkeys: Vec<PublicKey> = model
|
||||||
|
.read(cx)
|
||||||
|
.members
|
||||||
|
.iter()
|
||||||
|
.map(|m| m.public_key())
|
||||||
|
.collect();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
|
|
||||||
@@ -224,7 +217,7 @@ impl Chat {
|
|||||||
if !item.1 {
|
if !item.1 {
|
||||||
let name = this
|
let name = this
|
||||||
.room
|
.room
|
||||||
.read_with(cx, |this, _| this.name())
|
.read_with(cx, |this, _| this.name().unwrap_or("Unnamed".into()))
|
||||||
.unwrap_or("Unnamed".into());
|
.unwrap_or("Unnamed".into());
|
||||||
|
|
||||||
this.push_system_message(
|
this.push_system_message(
|
||||||
@@ -245,35 +238,25 @@ impl Chat {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let room = model.read(cx);
|
||||||
let client = get_client();
|
let client = get_client();
|
||||||
let (tx, rx) = oneshot::channel::<Events>();
|
let (tx, rx) = oneshot::channel::<Events>();
|
||||||
|
|
||||||
let room = model.read(cx);
|
|
||||||
let pubkeys = room
|
let pubkeys = room
|
||||||
.members
|
.members
|
||||||
.iter()
|
.iter()
|
||||||
.map(|m| m.public_key())
|
.map(|m| m.public_key())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let recv = Filter::new()
|
let filter = Filter::new()
|
||||||
.kind(Kind::PrivateDirectMessage)
|
.kind(Kind::PrivateDirectMessage)
|
||||||
.author(room.owner.public_key())
|
.authors(pubkeys.iter().copied())
|
||||||
.pubkeys(pubkeys.iter().copied());
|
.pubkeys(pubkeys);
|
||||||
|
|
||||||
let send = Filter::new()
|
|
||||||
.kind(Kind::PrivateDirectMessage)
|
|
||||||
.authors(pubkeys)
|
|
||||||
.pubkey(room.owner.public_key());
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
let Ok(recv_events) = client.database().query(recv).await else {
|
let Ok(events) = client.database().query(filter).await else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Ok(send_events) = client.database().query(send).await else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let events = recv_events.merge(send_events);
|
|
||||||
|
|
||||||
_ = tx.send(events);
|
_ = tx.send(events);
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
@@ -303,13 +286,13 @@ impl Chat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn push_message(&self, content: String, window: &mut Window, cx: &mut Context<Self>) {
|
fn push_message(&self, content: String, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Some(model) = self.room.upgrade() else {
|
let Some(account) = Account::global(cx) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let old_len = self.messages.read(cx).len();
|
let old_len = self.messages.read(cx).len();
|
||||||
let room = model.read(cx);
|
let profile = account.read(cx).get();
|
||||||
let message = Message::new(ParsedMessage::new(&room.owner, &content, Timestamp::now()));
|
let message = Message::new(ParsedMessage::new(profile, &content, Timestamp::now()));
|
||||||
|
|
||||||
// Update message list
|
// Update message list
|
||||||
cx.update_entity(&self.messages, |this, cx| {
|
cx.update_entity(&self.messages, |this, cx| {
|
||||||
@@ -333,39 +316,40 @@ impl Chat {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let old_len = self.messages.read(cx).len();
|
|
||||||
let room = model.read(cx);
|
let room = model.read(cx);
|
||||||
let pubkeys = room.pubkeys();
|
let pubkeys = room
|
||||||
|
.members
|
||||||
|
.iter()
|
||||||
|
.map(|m| m.public_key())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let (messages, total) = {
|
let old_len = self.messages.read(cx).len();
|
||||||
|
|
||||||
|
let (messages, new_len) = {
|
||||||
let items: Vec<Message> = events
|
let items: Vec<Message> = events
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.sorted_by_key(|ev| ev.created_at)
|
.sorted_by_key(|ev| ev.created_at)
|
||||||
.filter_map(|ev| {
|
.filter_map(|ev| {
|
||||||
let mut other_pubkeys: Vec<_> = ev.tags.public_keys().copied().collect();
|
let mut other_pubkeys = ev.tags.public_keys().copied().collect::<Vec<_>>();
|
||||||
other_pubkeys.push(ev.pubkey);
|
other_pubkeys.push(ev.pubkey);
|
||||||
|
|
||||||
if compare(&other_pubkeys, &pubkeys) {
|
if !compare(&other_pubkeys, &pubkeys) {
|
||||||
let member = if let Some(member) =
|
return None;
|
||||||
room.members.iter().find(|&m| m.public_key() == ev.pubkey)
|
|
||||||
{
|
|
||||||
member.to_owned()
|
|
||||||
} else {
|
|
||||||
room.owner.to_owned()
|
|
||||||
};
|
|
||||||
|
|
||||||
let message =
|
|
||||||
Message::new(ParsedMessage::new(&member, &ev.content, ev.created_at));
|
|
||||||
|
|
||||||
Some(message)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
room.members
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.public_key() == ev.pubkey)
|
||||||
|
.map(|member| {
|
||||||
|
Message::new(ParsedMessage::new(member, &ev.content, ev.created_at))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let total = items.len();
|
|
||||||
|
|
||||||
(items, total)
|
// Used for update list state
|
||||||
|
let new_len = items.len();
|
||||||
|
|
||||||
|
(items, new_len)
|
||||||
};
|
};
|
||||||
|
|
||||||
cx.update_entity(&self.messages, |this, cx| {
|
cx.update_entity(&self.messages, |this, cx| {
|
||||||
@@ -373,25 +357,27 @@ impl Chat {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
|
||||||
self.list_state.splice(old_len..old_len, total);
|
self.list_state.splice(old_len..old_len, new_len);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_new_messages(&mut self, cx: &mut Context<Self>) {
|
fn load_new_messages(&mut self, cx: &mut Context<Self>) {
|
||||||
let Some(Some(model)) = self.new_messages.as_ref().map(|state| state.upgrade()) else {
|
let Some(room) = self.room.upgrade() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let subscription = cx.observe(&model, |view, this, cx| {
|
let subscription = cx.observe(&room, |view, this, cx| {
|
||||||
let Some(model) = view.room.upgrade() else {
|
let room = this.read(cx);
|
||||||
|
|
||||||
|
if room.new_messages.is_empty() {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let room = model.read(cx);
|
|
||||||
let old_messages = view.messages.read(cx);
|
let old_messages = view.messages.read(cx);
|
||||||
let old_len = old_messages.len();
|
let old_len = old_messages.len();
|
||||||
|
|
||||||
let items: Vec<Message> = this
|
let items: Vec<Message> = this
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
.new_messages
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|event| {
|
.filter_map(|event| {
|
||||||
if let Some(profile) = room.member(&event.pubkey) {
|
if let Some(profile) = room.member(&event.pubkey) {
|
||||||
@@ -466,29 +452,33 @@ impl Chat {
|
|||||||
this.set_disabled(true, window, cx);
|
this.set_disabled(true, window, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let room = model.read(cx);
|
||||||
|
// let subject = Tag::from_standardized_without_cell(TagStandard::Subject(room.title.clone()));
|
||||||
|
let pubkeys = room.public_keys();
|
||||||
|
let async_content = content.clone().to_string();
|
||||||
|
|
||||||
let client = get_client();
|
let client = get_client();
|
||||||
let window_handle = window.window_handle();
|
let window_handle = window.window_handle();
|
||||||
let (tx, rx) = oneshot::channel::<Vec<Error>>();
|
let (tx, rx) = oneshot::channel::<Vec<Error>>();
|
||||||
|
|
||||||
let room = model.read(cx);
|
|
||||||
let pubkeys = room.pubkeys();
|
|
||||||
let async_content = content.clone().to_string();
|
|
||||||
let tags: Vec<Tag> = room
|
|
||||||
.pubkeys()
|
|
||||||
.iter()
|
|
||||||
.filter_map(|pubkey| {
|
|
||||||
if pubkey != &room.owner.public_key() {
|
|
||||||
Some(Tag::public_key(*pubkey))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Send message to all pubkeys
|
// Send message to all pubkeys
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
|
let signer = client.signer().await.unwrap();
|
||||||
|
let public_key = signer.get_public_key().await.unwrap();
|
||||||
|
|
||||||
let mut errors = Vec::new();
|
let mut errors = Vec::new();
|
||||||
|
|
||||||
|
let tags: Vec<Tag> = pubkeys
|
||||||
|
.iter()
|
||||||
|
.filter_map(|pubkey| {
|
||||||
|
if pubkey != &public_key {
|
||||||
|
Some(Tag::public_key(*pubkey))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
for pubkey in pubkeys.iter() {
|
for pubkey in pubkeys.iter() {
|
||||||
if let Err(e) = client
|
if let Err(e) = client
|
||||||
.send_private_msg(*pubkey, &async_content, tags.clone())
|
.send_private_msg(*pubkey, &async_content, tags.clone())
|
||||||
@@ -709,9 +699,8 @@ impl Panel for Chat {
|
|||||||
|
|
||||||
fn title(&self, cx: &App) -> AnyElement {
|
fn title(&self, cx: &App) -> AnyElement {
|
||||||
self.room
|
self.room
|
||||||
.read_with(cx, |this, _cx| {
|
.read_with(cx, |this, _| {
|
||||||
let name = this.name();
|
let facepill: Vec<SharedString> =
|
||||||
let facepill: Vec<String> =
|
|
||||||
this.members.iter().map(|member| member.avatar()).collect();
|
this.members.iter().map(|member| member.avatar()).collect();
|
||||||
|
|
||||||
div()
|
div()
|
||||||
@@ -733,20 +722,12 @@ impl Panel for Chat {
|
|||||||
)
|
)
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.child(name)
|
.when_some(this.name(), |this, name| this.child(name))
|
||||||
.into_any()
|
.into_any()
|
||||||
})
|
})
|
||||||
.unwrap_or("Unnamed".into_any())
|
.unwrap_or("Unnamed".into_any())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn closable(&self, _cx: &App) -> bool {
|
|
||||||
self.closable
|
|
||||||
}
|
|
||||||
|
|
||||||
fn zoomable(&self, _cx: &App) -> bool {
|
|
||||||
self.zoomable
|
|
||||||
}
|
|
||||||
|
|
||||||
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
||||||
menu.track_focus(&self.focus_handle)
|
menu.track_focus(&self.focus_handle)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,3 @@ mod welcome;
|
|||||||
|
|
||||||
pub mod app;
|
pub mod app;
|
||||||
pub mod onboarding;
|
pub mod onboarding;
|
||||||
pub mod startup;
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
use common::{profile::NostrProfile, qr::create_qr, utils::preload};
|
use account::registry::Account;
|
||||||
|
use common::qr::create_qr;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, prelude::FluentBuilder, relative, svg, App, AppContext, ClipboardItem, Context, Div,
|
div, img, prelude::FluentBuilder, relative, svg, App, AppContext, ClipboardItem, Context, Div,
|
||||||
Entity, IntoElement, ParentElement, Render, Styled, Subscription, Window,
|
Entity, IntoElement, ParentElement, Render, Styled, Subscription, Window,
|
||||||
};
|
};
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use state::get_client;
|
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||||
use std::{path::PathBuf, time::Duration};
|
|
||||||
use ui::{
|
use ui::{
|
||||||
button::{Button, ButtonCustomVariant, ButtonVariants},
|
button::{Button, ButtonCustomVariant, ButtonVariants},
|
||||||
input::{InputEvent, TextInput},
|
input::{InputEvent, TextInput},
|
||||||
@@ -16,8 +16,12 @@ use ui::{
|
|||||||
|
|
||||||
use super::app;
|
use super::app;
|
||||||
|
|
||||||
|
const LOGO_URL: &str = "brand/coop.svg";
|
||||||
|
const TITLE: &str = "Welcome to Coop!";
|
||||||
|
const SUBTITLE: &str = "A Nostr client for secure communication.";
|
||||||
const ALPHA_MESSAGE: &str =
|
const ALPHA_MESSAGE: &str =
|
||||||
"Coop is in the alpha stage of development; It may contain bugs, unfinished features, or unexpected behavior.";
|
"Coop is in the alpha stage of development; It may contain bugs, unfinished features, or unexpected behavior.";
|
||||||
|
|
||||||
const JOIN_URL: &str = "https://start.njump.me/";
|
const JOIN_URL: &str = "https://start.njump.me/";
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
|
||||||
@@ -62,7 +66,7 @@ impl Onboarding {
|
|||||||
window,
|
window,
|
||||||
move |this: &mut Self, _, input_event, window, cx| {
|
move |this: &mut Self, _, input_event, window, cx| {
|
||||||
if let InputEvent::PressEnter = input_event {
|
if let InputEvent::PressEnter = input_event {
|
||||||
this.privkey_login(window, cx);
|
this.login_with_private_key(window, cx);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)];
|
)];
|
||||||
@@ -80,68 +84,50 @@ impl Onboarding {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn use_connect(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn login_with_nostr_connect(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let uri = self.connect_uri.clone();
|
let uri = self.connect_uri.clone();
|
||||||
let app_keys = self.app_keys.clone();
|
let app_keys = self.app_keys.clone();
|
||||||
let window_handle = window.window_handle();
|
let window_handle = window.window_handle();
|
||||||
|
|
||||||
self.use_connect = true;
|
// Show QR Code for login with Nostr Connect
|
||||||
cx.notify();
|
self.use_connect(window, cx);
|
||||||
|
|
||||||
cx.spawn(|_, mut cx| async move {
|
// Wait for connection
|
||||||
let (tx, rx) = oneshot::channel::<NostrProfile>();
|
let (tx, rx) = oneshot::channel::<NostrConnect>();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
if let Ok(signer) = NostrConnect::new(uri, app_keys, Duration::from_secs(300), None)
|
if let Ok(signer) = NostrConnect::new(uri, app_keys, Duration::from_secs(300), None) {
|
||||||
{
|
tx.send(signer).ok();
|
||||||
if let Ok(uri) = signer.bunker_uri().await {
|
}
|
||||||
let client = get_client();
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
if let Some(public_key) = uri.remote_signer_public_key() {
|
cx.spawn(|this, cx| async move {
|
||||||
let metadata = client
|
if let Ok(signer) = rx.await {
|
||||||
.fetch_metadata(*public_key, Duration::from_secs(2))
|
cx.spawn(|mut cx| async move {
|
||||||
.await
|
let signer = Arc::new(signer);
|
||||||
.ok()
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
if tx.send(NostrProfile::new(*public_key, metadata)).is_ok() {
|
if Account::login(signer, &cx).await.is_ok() {
|
||||||
_ = client.set_signer(signer).await;
|
_ = cx.update_window(window_handle, |_, window, cx| {
|
||||||
_ = preload(client, *public_key).await;
|
window.replace_root(cx, |window, cx| {
|
||||||
}
|
Root::new(app::init(window, cx).into(), window, cx)
|
||||||
}
|
});
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
if let Ok(profile) = rx.await {
|
|
||||||
_ = cx.update_window(window_handle, |_, window, cx| {
|
|
||||||
window.replace_root(cx, |window, cx| {
|
|
||||||
Root::new(app::init(profile, window, cx).into(), window, cx)
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
|
.detach();
|
||||||
|
} else {
|
||||||
|
_ = cx.update(|cx| {
|
||||||
|
_ = this.update(cx, |this, cx| {
|
||||||
|
this.set_loading(false, cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn use_privkey(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
fn login_with_private_key(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.use_privkey = true;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.use_privkey = false;
|
|
||||||
self.use_connect = false;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
|
||||||
self.is_loading = status;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn privkey_login(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
let value = self.nsec_input.read(cx).text().to_string();
|
let value = self.nsec_input.read(cx).text().to_string();
|
||||||
let window_handle = window.window_handle();
|
let window_handle = window.window_handle();
|
||||||
|
|
||||||
@@ -160,37 +146,47 @@ impl Onboarding {
|
|||||||
// Show loading spinner
|
// Show loading spinner
|
||||||
self.set_loading(true, cx);
|
self.set_loading(true, cx);
|
||||||
|
|
||||||
cx.spawn(|_, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let client = get_client();
|
let signer = Arc::new(keys);
|
||||||
let (tx, rx) = oneshot::channel::<NostrProfile>();
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
if Account::login(signer, &cx).await.is_ok() {
|
||||||
if let Ok(public_key) = keys.get_public_key().await {
|
|
||||||
let metadata = client
|
|
||||||
.fetch_metadata(public_key, Duration::from_secs(2))
|
|
||||||
.await
|
|
||||||
.ok()
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
if tx.send(NostrProfile::new(public_key, metadata)).is_ok() {
|
|
||||||
_ = client.set_signer(keys).await;
|
|
||||||
_ = preload(client, public_key).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
if let Ok(profile) = rx.await {
|
|
||||||
_ = cx.update_window(window_handle, |_, window, cx| {
|
_ = cx.update_window(window_handle, |_, window, cx| {
|
||||||
window.replace_root(cx, |window, cx| {
|
window.replace_root(cx, |window, cx| {
|
||||||
Root::new(app::init(profile, window, cx).into(), window, cx)
|
Root::new(app::init(window, cx).into(), window, cx)
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
_ = cx.update(|cx| {
|
||||||
|
_ = this.update(cx, |this, cx| {
|
||||||
|
this.set_loading(false, cx);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn use_connect(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.use_connect = true;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn use_privkey(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.use_privkey = true;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.use_privkey = false;
|
||||||
|
self.use_connect = false;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||||
|
self.is_loading = status;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
fn render_selection(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
|
fn render_selection(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
|
||||||
div()
|
div()
|
||||||
.w_full()
|
.w_full()
|
||||||
@@ -205,7 +201,7 @@ impl Onboarding {
|
|||||||
.primary()
|
.primary()
|
||||||
.w_full()
|
.w_full()
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
this.use_connect(window, cx);
|
this.login_with_nostr_connect(window, cx);
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -331,7 +327,7 @@ impl Onboarding {
|
|||||||
.w_full()
|
.w_full()
|
||||||
.loading(self.is_loading)
|
.loading(self.is_loading)
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
this.privkey_login(window, cx);
|
this.login_with_private_key(window, cx);
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -368,7 +364,7 @@ impl Render for Onboarding {
|
|||||||
.gap_4()
|
.gap_4()
|
||||||
.child(
|
.child(
|
||||||
svg()
|
svg()
|
||||||
.path("brand/coop.svg")
|
.path(LOGO_URL)
|
||||||
.size_12()
|
.size_12()
|
||||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)),
|
.text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)),
|
||||||
)
|
)
|
||||||
@@ -380,7 +376,7 @@ impl Render for Onboarding {
|
|||||||
.text_lg()
|
.text_lg()
|
||||||
.font_semibold()
|
.font_semibold()
|
||||||
.line_height(relative(1.2))
|
.line_height(relative(1.2))
|
||||||
.child("Welcome to Coop!"),
|
.child(TITLE),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -388,19 +384,19 @@ impl Render for Onboarding {
|
|||||||
.text_color(
|
.text_color(
|
||||||
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
|
cx.theme().base.step(cx, ColorScaleStep::ELEVEN),
|
||||||
)
|
)
|
||||||
.child("A Nostr client for secure communication."),
|
.child(SUBTITLE),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(div().w_72().map(|_| {
|
.child(
|
||||||
if self.use_privkey {
|
div()
|
||||||
self.render_privkey_login(cx)
|
.w_72()
|
||||||
} else if self.use_connect {
|
.map(|_| match (self.use_privkey, self.use_connect) {
|
||||||
self.render_connect_login(cx)
|
(true, _) => self.render_privkey_login(cx),
|
||||||
} else {
|
(_, true) => self.render_connect_login(cx),
|
||||||
self.render_selection(window, cx)
|
_ => self.render_selection(window, cx),
|
||||||
}
|
}),
|
||||||
})),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
@@ -411,8 +407,8 @@ impl Render for Onboarding {
|
|||||||
.items_center()
|
.items_center()
|
||||||
.justify_center()
|
.justify_center()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
|
.text_center()
|
||||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
||||||
.text_align(gpui::TextAlign::Center)
|
|
||||||
.child(ALPHA_MESSAGE),
|
.child(ALPHA_MESSAGE),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use async_utility::task::spawn;
|
use async_utility::task::spawn;
|
||||||
use common::{constants::IMAGE_SERVICE, profile::NostrProfile, utils::nip96_upload};
|
use common::{constants::IMAGE_SERVICE, utils::nip96_upload};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, prelude::FluentBuilder, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
div, img, prelude::FluentBuilder, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||||
Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render,
|
Flatten, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render,
|
||||||
@@ -8,7 +8,7 @@ use gpui::{
|
|||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use smol::fs;
|
use smol::fs;
|
||||||
use state::get_client;
|
use state::get_client;
|
||||||
use std::str::FromStr;
|
use std::{str::FromStr, sync::Arc, time::Duration};
|
||||||
use ui::{
|
use ui::{
|
||||||
button::{Button, ButtonVariants},
|
button::{Button, ButtonVariants},
|
||||||
dock_area::panel::{Panel, PanelEvent},
|
dock_area::panel::{Panel, PanelEvent},
|
||||||
@@ -17,12 +17,12 @@ use ui::{
|
|||||||
ContextModal, Disableable, Sizable, Size,
|
ContextModal, Disableable, Sizable, Size,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(profile: NostrProfile, window: &mut Window, cx: &mut App) -> Entity<Profile> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Arc<Entity<Profile>> {
|
||||||
Profile::new(profile, window, cx)
|
Arc::new(Profile::new(window, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Profile {
|
pub struct Profile {
|
||||||
profile: NostrProfile,
|
profile: Option<Metadata>,
|
||||||
// Form
|
// Form
|
||||||
name_input: Entity<TextInput>,
|
name_input: Entity<TextInput>,
|
||||||
avatar_input: Entity<TextInput>,
|
avatar_input: Entity<TextInput>,
|
||||||
@@ -32,60 +32,108 @@ pub struct Profile {
|
|||||||
is_submitting: bool,
|
is_submitting: bool,
|
||||||
// Panel
|
// Panel
|
||||||
name: SharedString,
|
name: SharedString,
|
||||||
closable: bool,
|
|
||||||
zoomable: bool,
|
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Profile {
|
impl Profile {
|
||||||
pub fn new(mut profile: NostrProfile, window: &mut Window, cx: &mut App) -> Entity<Self> {
|
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||||
|
let window_handle = window.window_handle();
|
||||||
|
|
||||||
let name_input = cx.new(|cx| {
|
let name_input = cx.new(|cx| {
|
||||||
let mut input = TextInput::new(window, cx).text_size(Size::XSmall);
|
TextInput::new(window, cx)
|
||||||
if let Some(name) = profile.metadata().display_name.as_ref() {
|
|
||||||
input.set_text(name, window, cx);
|
|
||||||
}
|
|
||||||
input
|
|
||||||
});
|
|
||||||
let avatar_input = cx.new(|cx| {
|
|
||||||
let mut input = TextInput::new(window, cx).text_size(Size::XSmall).small();
|
|
||||||
if let Some(picture) = profile.metadata().picture.as_ref() {
|
|
||||||
input.set_text(picture, window, cx);
|
|
||||||
}
|
|
||||||
input
|
|
||||||
});
|
|
||||||
let bio_input = cx.new(|cx| {
|
|
||||||
let mut input = TextInput::new(window, cx)
|
|
||||||
.text_size(Size::XSmall)
|
.text_size(Size::XSmall)
|
||||||
.multi_line();
|
.placeholder("Alice")
|
||||||
if let Some(about) = profile.metadata().about.as_ref() {
|
|
||||||
input.set_text(about, window, cx);
|
|
||||||
} else {
|
|
||||||
input.set_placeholder("A short introduce about you.");
|
|
||||||
}
|
|
||||||
input
|
|
||||||
});
|
|
||||||
let website_input = cx.new(|cx| {
|
|
||||||
let mut input = TextInput::new(window, cx).text_size(Size::XSmall);
|
|
||||||
if let Some(website) = profile.metadata().website.as_ref() {
|
|
||||||
input.set_text(website, window, cx);
|
|
||||||
} else {
|
|
||||||
input.set_placeholder("https://your-website.com");
|
|
||||||
}
|
|
||||||
input
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.new(|cx| Self {
|
let avatar_input = cx.new(|cx| {
|
||||||
profile,
|
TextInput::new(window, cx)
|
||||||
name_input,
|
.text_size(Size::XSmall)
|
||||||
avatar_input,
|
.small()
|
||||||
bio_input,
|
.placeholder("https://example.com/avatar.png")
|
||||||
website_input,
|
});
|
||||||
is_loading: false,
|
|
||||||
is_submitting: false,
|
let website_input = cx.new(|cx| {
|
||||||
name: "Profile".into(),
|
TextInput::new(window, cx)
|
||||||
closable: true,
|
.text_size(Size::XSmall)
|
||||||
zoomable: true,
|
.placeholder("https://your-website.com")
|
||||||
focus_handle: cx.focus_handle(),
|
});
|
||||||
|
|
||||||
|
let bio_input = cx.new(|cx| {
|
||||||
|
TextInput::new(window, cx)
|
||||||
|
.text_size(Size::XSmall)
|
||||||
|
.multi_line()
|
||||||
|
.placeholder("A short introduce about you.")
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.new(|cx| {
|
||||||
|
let this = Self {
|
||||||
|
name_input,
|
||||||
|
avatar_input,
|
||||||
|
bio_input,
|
||||||
|
website_input,
|
||||||
|
profile: None,
|
||||||
|
is_loading: false,
|
||||||
|
is_submitting: false,
|
||||||
|
name: "Profile".into(),
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = get_client();
|
||||||
|
let (tx, rx) = oneshot::channel::<Option<Metadata>>();
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let result = async {
|
||||||
|
let signer = client.signer().await?;
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
let metadata = client
|
||||||
|
.fetch_metadata(public_key, Duration::from_secs(2))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok::<_, anyhow::Error>(metadata)
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Ok(metadata) = result {
|
||||||
|
_ = tx.send(Some(metadata));
|
||||||
|
} else {
|
||||||
|
_ = tx.send(None);
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
if let Ok(Some(metadata)) = rx.await {
|
||||||
|
_ = cx.update_window(window_handle, |_, window, cx| {
|
||||||
|
_ = this.update(cx, |this: &mut Profile, cx| {
|
||||||
|
this.avatar_input.update(cx, |this, cx| {
|
||||||
|
if let Some(avatar) = metadata.picture.as_ref() {
|
||||||
|
this.set_text(avatar, window, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.bio_input.update(cx, |this, cx| {
|
||||||
|
if let Some(bio) = metadata.about.as_ref() {
|
||||||
|
this.set_text(bio, window, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.name_input.update(cx, |this, cx| {
|
||||||
|
if let Some(display_name) = metadata.display_name.as_ref() {
|
||||||
|
this.set_text(display_name, window, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.website_input.update(cx, |this, cx| {
|
||||||
|
if let Some(website) = metadata.website.as_ref() {
|
||||||
|
this.set_text(website, window, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.profile = Some(metadata);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
this
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,12 +212,13 @@ impl Profile {
|
|||||||
let bio = self.bio_input.read(cx).text().to_string();
|
let bio = self.bio_input.read(cx).text().to_string();
|
||||||
let website = self.website_input.read(cx).text().to_string();
|
let website = self.website_input.read(cx).text().to_string();
|
||||||
|
|
||||||
let mut new_metadata = self
|
let old_metadata = if let Some(metadata) = self.profile.as_ref() {
|
||||||
.profile
|
metadata.clone()
|
||||||
.metadata()
|
} else {
|
||||||
.to_owned()
|
Metadata::default()
|
||||||
.display_name(name)
|
};
|
||||||
.about(bio);
|
|
||||||
|
let mut new_metadata = old_metadata.display_name(name).about(bio);
|
||||||
|
|
||||||
if let Ok(url) = Url::from_str(&avatar) {
|
if let Ok(url) = Url::from_str(&avatar) {
|
||||||
new_metadata = new_metadata.picture(url);
|
new_metadata = new_metadata.picture(url);
|
||||||
@@ -221,14 +270,6 @@ impl Panel for Profile {
|
|||||||
self.name.clone().into_any_element()
|
self.name.clone().into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn closable(&self, _cx: &App) -> bool {
|
|
||||||
self.closable
|
|
||||||
}
|
|
||||||
|
|
||||||
fn zoomable(&self, _cx: &App) -> bool {
|
|
||||||
self.zoomable
|
|
||||||
}
|
|
||||||
|
|
||||||
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
||||||
menu.track_focus(&self.focus_handle)
|
menu.track_focus(&self.focus_handle)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
use async_utility::task::spawn;
|
use async_utility::task::spawn;
|
||||||
use chats::{registry::ChatRegistry, room::Room};
|
use chats::{registry::ChatRegistry, room::Room};
|
||||||
use common::{
|
use common::{profile::NostrProfile, utils::random_name};
|
||||||
profile::NostrProfile,
|
|
||||||
utils::{random_name, signer_public_key},
|
|
||||||
};
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, impl_internal_actions, prelude::FluentBuilder, px, relative, uniform_list, App,
|
div, img, impl_internal_actions, prelude::FluentBuilder, px, relative, uniform_list, App,
|
||||||
AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement, ParentElement,
|
AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement, ParentElement,
|
||||||
@@ -86,15 +83,16 @@ impl Compose {
|
|||||||
let (tx, rx) = oneshot::channel::<Vec<NostrProfile>>();
|
let (tx, rx) = oneshot::channel::<Vec<NostrProfile>>();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
if let Ok(public_key) = signer_public_key(client).await {
|
let signer = client.signer().await.unwrap();
|
||||||
if let Ok(profiles) = client.database().contacts(public_key).await {
|
let public_key = signer.get_public_key().await.unwrap();
|
||||||
let members: Vec<NostrProfile> = profiles
|
|
||||||
.into_iter()
|
|
||||||
.map(|profile| NostrProfile::new(profile.public_key(), profile.metadata()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
_ = tx.send(members);
|
if let Ok(profiles) = client.database().contacts(public_key).await {
|
||||||
}
|
let members: Vec<NostrProfile> = profiles
|
||||||
|
.into_iter()
|
||||||
|
.map(|profile| NostrProfile::new(profile.public_key(), profile.metadata()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
_ = tx.send(members);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
@@ -178,17 +176,19 @@ impl Compose {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if let Some(chats) = ChatRegistry::global(cx) {
|
if let Some(chats) = ChatRegistry::global(cx) {
|
||||||
let room = Room::parse(&event, cx);
|
let room = Room::new(&event, cx);
|
||||||
|
|
||||||
chats.update(cx, |state, cx| match state.new_room(room, cx) {
|
chats.update(cx, |state, cx| {
|
||||||
Ok(_) => {
|
match state.push_room(room, cx) {
|
||||||
// TODO: open chat panel
|
Ok(_) => {
|
||||||
window.close_modal(cx);
|
// TODO: open chat panel
|
||||||
}
|
window.close_modal(cx);
|
||||||
Err(e) => {
|
}
|
||||||
_ = this.update(cx, |this, cx| {
|
Err(e) => {
|
||||||
this.set_error(Some(e.to_string().into()), cx);
|
_ = this.update(cx, |this, cx| {
|
||||||
});
|
this.set_error(Some(e.to_string().into()), cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,182 +0,0 @@
|
|||||||
use crate::views::app::{AddPanel, PanelKind};
|
|
||||||
use chats::registry::ChatRegistry;
|
|
||||||
use gpui::{
|
|
||||||
div, img, percentage, prelude::FluentBuilder, px, relative, Context, InteractiveElement,
|
|
||||||
IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled,
|
|
||||||
TextAlign, Window,
|
|
||||||
};
|
|
||||||
use ui::{
|
|
||||||
dock_area::dock::DockPlacement,
|
|
||||||
skeleton::Skeleton,
|
|
||||||
theme::{scale::ColorScaleStep, ActiveTheme},
|
|
||||||
v_flex, Collapsible, Icon, IconName, StyledExt,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct Inbox {
|
|
||||||
label: SharedString,
|
|
||||||
is_collapsed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Inbox {
|
|
||||||
pub fn new(_window: &mut Window, _cx: &mut Context<'_, Self>) -> Self {
|
|
||||||
Self {
|
|
||||||
label: "Inbox".into(),
|
|
||||||
is_collapsed: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_skeleton(&self, total: i32) -> impl IntoIterator<Item = impl IntoElement> {
|
|
||||||
(0..total).map(|_| {
|
|
||||||
div()
|
|
||||||
.h_8()
|
|
||||||
.px_1()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.gap_2()
|
|
||||||
.child(Skeleton::new().flex_shrink_0().size_6().rounded_full())
|
|
||||||
.child(Skeleton::new().w_20().h_3().rounded_sm())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_item(&self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
if let Some(chats) = ChatRegistry::global(cx) {
|
|
||||||
div().map(|this| {
|
|
||||||
let state = chats.read(cx);
|
|
||||||
let rooms = state.rooms();
|
|
||||||
|
|
||||||
if state.is_loading() {
|
|
||||||
this.children(self.render_skeleton(5))
|
|
||||||
} else if rooms.is_empty() {
|
|
||||||
this.px_1()
|
|
||||||
.w_full()
|
|
||||||
.h_20()
|
|
||||||
.flex()
|
|
||||||
.flex_col()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.text_align(TextAlign::Center)
|
|
||||||
.rounded(px(cx.theme().radius))
|
|
||||||
.bg(cx.theme().base.step(cx, ColorScaleStep::THREE))
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.font_semibold()
|
|
||||||
.line_height(relative(1.2))
|
|
||||||
.child("No chats"),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
|
||||||
.child("Recent chats will appear here."),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this.children(rooms.iter().map(|model| {
|
|
||||||
let room = model.read(cx);
|
|
||||||
let room_id: SharedString = room.id.to_string().into();
|
|
||||||
|
|
||||||
div()
|
|
||||||
.id(room_id)
|
|
||||||
.h_8()
|
|
||||||
.px_1()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.justify_between()
|
|
||||||
.text_xs()
|
|
||||||
.rounded(px(cx.theme().radius))
|
|
||||||
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::FOUR)))
|
|
||||||
.child(div().flex_1().truncate().font_medium().map(|this| {
|
|
||||||
if room.is_group {
|
|
||||||
this.flex()
|
|
||||||
.items_center()
|
|
||||||
.gap_2()
|
|
||||||
.child(img("brand/avatar.png").size_6().rounded_full())
|
|
||||||
.child(room.name())
|
|
||||||
} else {
|
|
||||||
this.when_some(room.members.first(), |this, sender| {
|
|
||||||
this.flex()
|
|
||||||
.items_center()
|
|
||||||
.gap_2()
|
|
||||||
.child(
|
|
||||||
img(sender.avatar())
|
|
||||||
.size_6()
|
|
||||||
.rounded_full()
|
|
||||||
.flex_shrink_0(),
|
|
||||||
)
|
|
||||||
.child(sender.name())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex_shrink_0()
|
|
||||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
|
||||||
.child(room.last_seen.ago()),
|
|
||||||
)
|
|
||||||
.on_click({
|
|
||||||
let id = room.id;
|
|
||||||
cx.listener(move |this, _, window, cx| {
|
|
||||||
this.action(id, window, cx);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
div().children(self.render_skeleton(5))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn action(&self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
window.dispatch_action(
|
|
||||||
Box::new(AddPanel::new(PanelKind::Room(id), DockPlacement::Center)),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Collapsible for Inbox {
|
|
||||||
fn collapsed(mut self, collapsed: bool) -> Self {
|
|
||||||
self.is_collapsed = collapsed;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_collapsed(&self) -> bool {
|
|
||||||
self.is_collapsed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for Inbox {
|
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
v_flex()
|
|
||||||
.px_2()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.id("inbox")
|
|
||||||
.h_7()
|
|
||||||
.px_1()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.rounded(px(cx.theme().radius))
|
|
||||||
.text_xs()
|
|
||||||
.font_semibold()
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::ChevronDown)
|
|
||||||
.size_6()
|
|
||||||
.when(self.is_collapsed, |this| {
|
|
||||||
this.rotate(percentage(270. / 360.))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(self.label.clone())
|
|
||||||
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)))
|
|
||||||
.on_click(cx.listener(move |view, _event, _window, cx| {
|
|
||||||
view.is_collapsed = !view.is_collapsed;
|
|
||||||
cx.notify();
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.when(!self.is_collapsed, |this| {
|
|
||||||
this.child(self.render_item(window, cx))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +1,32 @@
|
|||||||
use crate::views::sidebar::inbox::Inbox;
|
use chats::{registry::ChatRegistry, room::Room};
|
||||||
use compose::Compose;
|
use compose::Compose;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, px, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
div, img, percentage, prelude::FluentBuilder, px, uniform_list, AnyElement, App, AppContext,
|
||||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
Context, Div, Empty, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
|
||||||
StatefulInteractiveElement, Styled, Window,
|
IntoElement, ParentElement, Render, SharedString, Stateful, StatefulInteractiveElement, Styled,
|
||||||
|
Window,
|
||||||
};
|
};
|
||||||
use ui::{
|
use ui::{
|
||||||
button::{Button, ButtonRounded, ButtonVariants},
|
button::{Button, ButtonRounded, ButtonVariants},
|
||||||
dock_area::panel::{Panel, PanelEvent},
|
dock_area::panel::{Panel, PanelEvent},
|
||||||
popup_menu::PopupMenu,
|
popup_menu::PopupMenu,
|
||||||
theme::{scale::ColorScaleStep, ActiveTheme},
|
theme::{scale::ColorScaleStep, ActiveTheme},
|
||||||
v_flex, ContextModal, Disableable, Icon, IconName, Sizable, StyledExt,
|
ContextModal, Disableable, Icon, IconName, Sizable, StyledExt,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::app::AddPanel;
|
||||||
|
|
||||||
mod compose;
|
mod compose;
|
||||||
mod inbox;
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
|
||||||
Sidebar::new(window, cx)
|
Sidebar::new(window, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Sidebar {
|
pub struct Sidebar {
|
||||||
// Panel
|
|
||||||
name: SharedString,
|
name: SharedString,
|
||||||
closable: bool,
|
|
||||||
zoomable: bool,
|
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
// Dock
|
label: SharedString,
|
||||||
inbox: Entity<Inbox>,
|
is_collapsed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sidebar {
|
impl Sidebar {
|
||||||
@@ -35,19 +34,19 @@ impl Sidebar {
|
|||||||
cx.new(|cx| Self::view(window, cx))
|
cx.new(|cx| Self::view(window, cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn view(_window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let inbox = cx.new(|cx| Inbox::new(window, cx));
|
let focus_handle = cx.focus_handle();
|
||||||
|
let label = SharedString::from("Inbox");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
name: "Sidebar".into(),
|
name: "Sidebar".into(),
|
||||||
closable: true,
|
is_collapsed: false,
|
||||||
zoomable: true,
|
focus_handle,
|
||||||
focus_handle: cx.focus_handle(),
|
label,
|
||||||
inbox,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_compose(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn render_compose(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let compose = cx.new(|cx| Compose::new(window, cx));
|
let compose = cx.new(|cx| Compose::new(window, cx));
|
||||||
|
|
||||||
window.open_modal(cx, move |modal, window, cx| {
|
window.open_modal(cx, move |modal, window, cx| {
|
||||||
@@ -79,6 +78,73 @@ impl Sidebar {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_room(&self, ix: usize, room: &Entity<Room>, cx: &Context<Self>) -> Stateful<Div> {
|
||||||
|
let room = room.read(cx);
|
||||||
|
|
||||||
|
div()
|
||||||
|
.id(ix)
|
||||||
|
.px_1()
|
||||||
|
.h_8()
|
||||||
|
.w_full()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.text_xs()
|
||||||
|
.rounded(px(cx.theme().radius))
|
||||||
|
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::FOUR)))
|
||||||
|
.child(div().flex_1().truncate().font_medium().map(|this| {
|
||||||
|
if room.is_group() {
|
||||||
|
this.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.justify_center()
|
||||||
|
.items_center()
|
||||||
|
.size_6()
|
||||||
|
.rounded_full()
|
||||||
|
.bg(cx.theme().accent.step(cx, ColorScaleStep::THREE))
|
||||||
|
.child(Icon::new(IconName::GroupFill).size_3().text_color(
|
||||||
|
cx.theme().accent.step(cx, ColorScaleStep::TWELVE),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.when_some(room.name(), |this, name| this.child(name))
|
||||||
|
} else {
|
||||||
|
this.when_some(room.first_member(), |this, member| {
|
||||||
|
this.flex()
|
||||||
|
.items_center()
|
||||||
|
.gap_2()
|
||||||
|
.child(img(member.avatar()).size_6().rounded_full().flex_shrink_0())
|
||||||
|
.child(member.name())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.flex_shrink_0()
|
||||||
|
.text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN))
|
||||||
|
.child(room.ago()),
|
||||||
|
)
|
||||||
|
.on_click({
|
||||||
|
let id = room.id;
|
||||||
|
|
||||||
|
cx.listener(move |this, _, window, cx| {
|
||||||
|
this.open(id, window, cx);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open(&self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
window.dispatch_action(
|
||||||
|
Box::new(AddPanel::new(
|
||||||
|
super::app::PanelKind::Room(id),
|
||||||
|
ui::dock_area::dock::DockPlacement::Center,
|
||||||
|
)),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Panel for Sidebar {
|
impl Panel for Sidebar {
|
||||||
@@ -90,14 +156,6 @@ impl Panel for Sidebar {
|
|||||||
self.name.clone().into_any_element()
|
self.name.clone().into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn closable(&self, _cx: &App) -> bool {
|
|
||||||
self.closable
|
|
||||||
}
|
|
||||||
|
|
||||||
fn zoomable(&self, _cx: &App) -> bool {
|
|
||||||
self.zoomable
|
|
||||||
}
|
|
||||||
|
|
||||||
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu {
|
||||||
menu.track_focus(&self.focus_handle)
|
menu.track_focus(&self.focus_handle)
|
||||||
}
|
}
|
||||||
@@ -117,41 +175,116 @@ impl Focusable for Sidebar {
|
|||||||
|
|
||||||
impl Render for Sidebar {
|
impl Render for Sidebar {
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
v_flex()
|
let entity = cx.entity();
|
||||||
.w_full()
|
|
||||||
.py_3()
|
div()
|
||||||
.gap_3()
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.size_full()
|
||||||
.child(
|
.child(
|
||||||
v_flex().px_2().gap_1().child(
|
div()
|
||||||
div()
|
.px_2()
|
||||||
.id("new")
|
.py_3()
|
||||||
.flex()
|
.w_full()
|
||||||
.items_center()
|
.flex_shrink_0()
|
||||||
.gap_2()
|
.flex()
|
||||||
.px_1()
|
.flex_col()
|
||||||
.h_7()
|
.gap_1()
|
||||||
.text_xs()
|
.child(
|
||||||
.font_semibold()
|
div()
|
||||||
.rounded(px(cx.theme().radius))
|
.id("new_message")
|
||||||
.child(
|
.flex()
|
||||||
div()
|
.items_center()
|
||||||
.size_6()
|
.gap_2()
|
||||||
.flex()
|
.px_1()
|
||||||
.items_center()
|
.h_7()
|
||||||
.justify_center()
|
.text_xs()
|
||||||
.rounded_full()
|
.font_semibold()
|
||||||
.bg(cx.theme().accent.step(cx, ColorScaleStep::NINE))
|
.rounded(px(cx.theme().radius))
|
||||||
.child(
|
.child(
|
||||||
Icon::new(IconName::ComposeFill)
|
div()
|
||||||
.small()
|
.size_6()
|
||||||
.text_color(cx.theme().base.darken(cx)),
|
.flex()
|
||||||
),
|
.items_center()
|
||||||
)
|
.justify_center()
|
||||||
.child("New Message")
|
.rounded_full()
|
||||||
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)))
|
.bg(cx.theme().accent.step(cx, ColorScaleStep::NINE))
|
||||||
.on_click(cx.listener(|this, _, window, cx| this.show_compose(window, cx))),
|
.child(
|
||||||
),
|
Icon::new(IconName::ComposeFill)
|
||||||
|
.small()
|
||||||
|
.text_color(cx.theme().base.darken(cx)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child("New Message")
|
||||||
|
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)))
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
// Open compose modal
|
||||||
|
this.render_compose(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(Empty),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.px_2()
|
||||||
|
.w_full()
|
||||||
|
.flex_1()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.id("inbox_header")
|
||||||
|
.px_1()
|
||||||
|
.h_7()
|
||||||
|
.flex()
|
||||||
|
.items_center()
|
||||||
|
.flex_shrink_0()
|
||||||
|
.rounded(px(cx.theme().radius))
|
||||||
|
.text_xs()
|
||||||
|
.font_semibold()
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::ChevronDown)
|
||||||
|
.size_6()
|
||||||
|
.when(self.is_collapsed, |this| {
|
||||||
|
this.rotate(percentage(270. / 360.))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(self.label.clone())
|
||||||
|
.hover(|this| this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)))
|
||||||
|
.on_click(cx.listener(move |view, _event, _window, cx| {
|
||||||
|
view.is_collapsed = !view.is_collapsed;
|
||||||
|
cx.notify();
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.when(!self.is_collapsed, |this| {
|
||||||
|
this.flex_1()
|
||||||
|
.w_full()
|
||||||
|
.when_some(ChatRegistry::global(cx), |this, state| {
|
||||||
|
let rooms = state.read(cx).rooms();
|
||||||
|
let len = rooms.len();
|
||||||
|
|
||||||
|
this.child(
|
||||||
|
uniform_list(
|
||||||
|
entity,
|
||||||
|
"rooms",
|
||||||
|
len,
|
||||||
|
move |this, range, _, cx| {
|
||||||
|
let mut items = vec![];
|
||||||
|
|
||||||
|
for ix in range {
|
||||||
|
if let Some(room) = rooms.get(ix) {
|
||||||
|
items.push(this.render_room(ix, room, cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.size_full(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.child(self.inbox.clone())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
use gpui::{
|
|
||||||
div, svg, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, Styled, Window,
|
|
||||||
};
|
|
||||||
use ui::theme::{scale::ColorScaleStep, ActiveTheme};
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Startup> {
|
|
||||||
Startup::new(window, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Startup {}
|
|
||||||
|
|
||||||
impl Startup {
|
|
||||||
pub fn new(_window: &mut Window, cx: &mut App) -> Entity<Self> {
|
|
||||||
cx.new(|_| Self {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for Startup {
|
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
div()
|
|
||||||
.size_full()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.child(
|
|
||||||
svg()
|
|
||||||
.path("brand/coop.svg")
|
|
||||||
.size_12()
|
|
||||||
.text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,5 +13,6 @@ nostr-sdk.workspace = true
|
|||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
|
smallvec.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
oneshot.workspace = true
|
oneshot.workspace = true
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
|
use crate::room::Room;
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use common::utils::{compare, room_hash, signer_public_key};
|
use common::{last_seen::LastSeen, utils::room_hash};
|
||||||
use gpui::{App, AppContext, Context, Entity, Global, WeakEntity};
|
use gpui::{App, AppContext, Context, Entity, Global, WeakEntity};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use state::get_client;
|
use state::get_client;
|
||||||
use std::cmp::Reverse;
|
use std::{cmp::Reverse, rc::Rc, sync::RwLock};
|
||||||
|
|
||||||
use crate::room::Room;
|
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
ChatRegistry::register(cx);
|
ChatRegistry::register(cx);
|
||||||
@@ -17,7 +16,7 @@ struct GlobalChatRegistry(Entity<ChatRegistry>);
|
|||||||
impl Global for GlobalChatRegistry {}
|
impl Global for GlobalChatRegistry {}
|
||||||
|
|
||||||
pub struct ChatRegistry {
|
pub struct ChatRegistry {
|
||||||
rooms: Vec<Entity<Room>>,
|
rooms: Rc<RwLock<Vec<Entity<Room>>>>,
|
||||||
is_loading: bool,
|
is_loading: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,83 +39,67 @@ impl ChatRegistry {
|
|||||||
// Set global state
|
// Set global state
|
||||||
cx.set_global(GlobalChatRegistry(entity.clone()));
|
cx.set_global(GlobalChatRegistry(entity.clone()));
|
||||||
|
|
||||||
// Observe and load metadata for any new rooms
|
|
||||||
cx.observe_new::<Room>(|this, _window, cx| {
|
|
||||||
let client = get_client();
|
|
||||||
let pubkeys = this.pubkeys();
|
|
||||||
let (tx, rx) = oneshot::channel::<Vec<(PublicKey, Metadata)>>();
|
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let mut profiles = Vec::new();
|
|
||||||
|
|
||||||
for public_key in pubkeys.into_iter() {
|
|
||||||
if let Ok(metadata) = client.database().metadata(public_key).await {
|
|
||||||
profiles.push((public_key, metadata.unwrap_or_default()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = tx.send(profiles);
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
|
||||||
if let Ok(profiles) = rx.await {
|
|
||||||
if let Some(room) = this.upgrade() {
|
|
||||||
_ = cx.update_entity(&room, |this, cx| {
|
|
||||||
for profile in profiles.into_iter() {
|
|
||||||
this.set_metadata(profile.0, profile.1);
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
entity
|
entity
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(_cx: &mut Context<Self>) -> Self {
|
fn new(_cx: &mut Context<Self>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
rooms: vec![],
|
rooms: Rc::new(RwLock::new(vec![])),
|
||||||
is_loading: true,
|
is_loading: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_rooms_ids(&self, cx: &mut Context<Self>) -> Vec<u64> {
|
pub fn current_rooms_ids(&self, cx: &mut Context<Self>) -> Vec<u64> {
|
||||||
self.rooms.iter().map(|room| room.read(cx).id).collect()
|
self.rooms
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
|
.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, cx: &mut Context<Self>) {
|
||||||
let client = get_client();
|
let client = get_client();
|
||||||
let (tx, rx) = oneshot::channel::<Vec<Event>>();
|
let (tx, rx) = oneshot::channel::<Option<Vec<Event>>>();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
cx.background_spawn(async move {
|
||||||
if let Ok(public_key) = signer_public_key(client).await {
|
let result = async {
|
||||||
let filter = Filter::new()
|
let signer = client.signer().await?;
|
||||||
|
let public_key = signer.get_public_key().await?;
|
||||||
|
|
||||||
|
let send = Filter::new()
|
||||||
.kind(Kind::PrivateDirectMessage)
|
.kind(Kind::PrivateDirectMessage)
|
||||||
.author(public_key);
|
.author(public_key);
|
||||||
|
|
||||||
// Get all DM events from database
|
let recv = Filter::new()
|
||||||
if let Ok(events) = client.database().query(filter).await {
|
.kind(Kind::PrivateDirectMessage)
|
||||||
let result: Vec<Event> = events
|
.pubkey(public_key);
|
||||||
.into_iter()
|
|
||||||
.filter(|ev| ev.tags.public_keys().peekable().peek().is_some())
|
|
||||||
.unique_by(room_hash)
|
|
||||||
.sorted_by_key(|ev| Reverse(ev.created_at))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
_ = tx.send(result);
|
let send_events = client.database().query(send).await?;
|
||||||
}
|
let recv_events = client.database().query(recv).await?;
|
||||||
|
|
||||||
|
Ok::<_, anyhow::Error>(send_events.merge(recv_events))
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Ok(events) = result {
|
||||||
|
let result: Vec<Event> = events
|
||||||
|
.into_iter()
|
||||||
|
.filter(|ev| ev.tags.public_keys().peekable().peek().is_some())
|
||||||
|
.unique_by(room_hash)
|
||||||
|
.sorted_by_key(|ev| Reverse(ev.created_at))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
_ = tx.send(Some(result));
|
||||||
|
} else {
|
||||||
|
_ = tx.send(None);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
cx.spawn(|this, cx| async move {
|
cx.spawn(|this, cx| async move {
|
||||||
if let Ok(events) = rx.await {
|
if let Ok(Some(events)) = rx.await {
|
||||||
if !events.is_empty() {
|
if !events.is_empty() {
|
||||||
_ = cx.update(|cx| {
|
_ = cx.update(|cx| {
|
||||||
_ = this.update(cx, |this, cx| {
|
_ = this.update(cx, |this, cx| {
|
||||||
@@ -127,14 +110,14 @@ impl ChatRegistry {
|
|||||||
let new = room_hash(&ev);
|
let new = room_hash(&ev);
|
||||||
// Filter all seen events
|
// Filter all seen events
|
||||||
if !current_rooms.iter().any(|this| this == &new) {
|
if !current_rooms.iter().any(|this| this == &new) {
|
||||||
Some(cx.new(|cx| Room::parse(&ev, cx)))
|
Some(Room::new(&ev, cx))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
this.rooms.extend(items);
|
this.rooms.write().unwrap().extend(items);
|
||||||
this.is_loading = false;
|
this.is_loading = false;
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -146,8 +129,8 @@ impl ChatRegistry {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rooms(&self) -> &Vec<Entity<Room>> {
|
pub fn rooms(&self) -> Vec<Entity<Room>> {
|
||||||
&self.rooms
|
self.rooms.read().unwrap().clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_loading(&self) -> bool {
|
pub fn is_loading(&self) -> bool {
|
||||||
@@ -156,18 +139,25 @@ impl ChatRegistry {
|
|||||||
|
|
||||||
pub fn get(&self, id: &u64, cx: &App) -> Option<WeakEntity<Room>> {
|
pub fn get(&self, id: &u64, cx: &App) -> Option<WeakEntity<Room>> {
|
||||||
self.rooms
|
self.rooms
|
||||||
|
.read()
|
||||||
|
.unwrap()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|model| model.read(cx).id == *id)
|
.find(|model| model.read(cx).id == *id)
|
||||||
.map(|room| room.downgrade())
|
.map(|room| room.downgrade())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_room(&mut self, room: Room, cx: &mut Context<Self>) -> Result<(), anyhow::Error> {
|
pub fn push_room(
|
||||||
if !self
|
&mut self,
|
||||||
.rooms
|
room: Entity<Room>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Result<(), anyhow::Error> {
|
||||||
|
let mut rooms = self.rooms.write().unwrap();
|
||||||
|
|
||||||
|
if !rooms
|
||||||
.iter()
|
.iter()
|
||||||
.any(|current| compare(¤t.read(cx).pubkeys(), &room.pubkeys()))
|
.any(|current| current.read(cx) == room.read(cx))
|
||||||
{
|
{
|
||||||
self.rooms.insert(0, cx.new(|_| room));
|
rooms.insert(0, room);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -177,32 +167,27 @@ impl ChatRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_message(&mut self, event: Event, cx: &mut Context<Self>) {
|
pub fn push_message(&mut self, event: Event, cx: &mut Context<Self>) {
|
||||||
// Get all pubkeys from event's tags for comparision
|
let id = room_hash(&event);
|
||||||
let mut pubkeys: Vec<_> = event.tags.public_keys().copied().collect();
|
let mut rooms = self.rooms.write().unwrap();
|
||||||
pubkeys.push(event.pubkey);
|
|
||||||
|
|
||||||
if let Some(room) = self
|
if let Some(room) = rooms.iter().find(|room| room.read(cx).id == id) {
|
||||||
.rooms
|
|
||||||
.iter()
|
|
||||||
.find(|room| compare(&room.read(cx).pubkeys(), &pubkeys))
|
|
||||||
{
|
|
||||||
room.update(cx, |this, cx| {
|
room.update(cx, |this, cx| {
|
||||||
this.last_seen.set(event.created_at);
|
if let Some(last_seen) = Rc::get_mut(&mut this.last_seen) {
|
||||||
this.new_messages.update(cx, |this, cx| {
|
*last_seen = LastSeen(event.created_at);
|
||||||
this.push(event);
|
}
|
||||||
cx.notify();
|
this.new_messages.push(event);
|
||||||
});
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re sort rooms by last seen
|
// Re sort rooms by last seen
|
||||||
self.rooms
|
rooms.sort_by_key(|room| Reverse(room.read(cx).last_seen()));
|
||||||
.sort_by_key(|room| Reverse(room.read(cx).last_seen()));
|
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
} else {
|
} else {
|
||||||
let room = cx.new(|cx| Room::parse(&event, cx));
|
let mut rooms = self.rooms.write().unwrap();
|
||||||
self.rooms.insert(0, room);
|
let new_room = Room::new(&event, cx);
|
||||||
|
|
||||||
|
rooms.insert(0, new_room);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,138 +1,161 @@
|
|||||||
use common::{
|
use common::{
|
||||||
last_seen::LastSeen,
|
last_seen::LastSeen,
|
||||||
profile::NostrProfile,
|
profile::NostrProfile,
|
||||||
utils::{compare, random_name, room_hash},
|
utils::{random_name, room_hash},
|
||||||
};
|
};
|
||||||
use gpui::{App, AppContext, Entity, SharedString};
|
use gpui::{App, AppContext, Entity, SharedString};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use std::collections::HashSet;
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
use state::get_client;
|
||||||
|
use std::{collections::HashSet, rc::Rc};
|
||||||
|
|
||||||
pub struct Room {
|
pub struct Room {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
pub title: Option<SharedString>,
|
pub last_seen: Rc<LastSeen>,
|
||||||
pub owner: NostrProfile, // Owner always match current user
|
/// Subject of the room (Nostr)
|
||||||
pub members: Vec<NostrProfile>, // Extract from event's tags
|
pub title: String,
|
||||||
pub last_seen: LastSeen,
|
/// Display name of the room (used for display purposes in Coop)
|
||||||
pub is_group: bool,
|
pub display_name: Option<SharedString>,
|
||||||
pub new_messages: Entity<Vec<Event>>, // Hold all new messages
|
/// All members of the room
|
||||||
|
pub members: SmallVec<[NostrProfile; 2]>,
|
||||||
|
/// Store all new messages
|
||||||
|
pub new_messages: Vec<Event>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Room {
|
impl PartialEq for Room {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
compare(&self.pubkeys(), &other.pubkeys())
|
self.id == other.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Room {
|
impl Room {
|
||||||
pub fn new(
|
pub fn new(event: &Event, cx: &mut App) -> Entity<Self> {
|
||||||
id: u64,
|
|
||||||
owner: NostrProfile,
|
|
||||||
members: Vec<NostrProfile>,
|
|
||||||
title: Option<SharedString>,
|
|
||||||
last_seen: LastSeen,
|
|
||||||
cx: &mut App,
|
|
||||||
) -> Self {
|
|
||||||
let new_messages = cx.new(|_| Vec::new());
|
|
||||||
let is_group = members.len() > 1;
|
|
||||||
let title = if title.is_none() {
|
|
||||||
Some(random_name(2).into())
|
|
||||||
} else {
|
|
||||||
title
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
owner,
|
|
||||||
members,
|
|
||||||
title,
|
|
||||||
last_seen,
|
|
||||||
is_group,
|
|
||||||
new_messages,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert nostr event to room
|
|
||||||
pub fn parse(event: &Event, cx: &mut App) -> Room {
|
|
||||||
let id = room_hash(event);
|
let id = room_hash(event);
|
||||||
let last_seen = LastSeen(event.created_at);
|
let last_seen = Rc::new(LastSeen(event.created_at));
|
||||||
|
// Get the subject from the event's tags, or create a random subject if none is found
|
||||||
// Always equal to current user
|
|
||||||
let owner = NostrProfile::new(event.pubkey, Metadata::default());
|
|
||||||
|
|
||||||
// Get all pubkeys that invole in this group
|
|
||||||
let members: Vec<NostrProfile> = event
|
|
||||||
.tags
|
|
||||||
.public_keys()
|
|
||||||
.collect::<HashSet<_>>()
|
|
||||||
.into_iter()
|
|
||||||
.map(|public_key| NostrProfile::new(*public_key, Metadata::default()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Get title from event's tags
|
|
||||||
let title = if let Some(tag) = event.tags.find(TagKind::Subject) {
|
let title = if let Some(tag) = event.tags.find(TagKind::Subject) {
|
||||||
tag.content().map(|s| s.to_owned().into())
|
tag.content()
|
||||||
|
.map(|s| s.to_owned())
|
||||||
|
.unwrap_or(random_name(2))
|
||||||
} else {
|
} else {
|
||||||
None
|
random_name(2)
|
||||||
};
|
};
|
||||||
|
|
||||||
Self::new(id, owner, members, title, last_seen, cx)
|
let room = cx.new(|cx| {
|
||||||
|
let this = Self {
|
||||||
|
id,
|
||||||
|
last_seen,
|
||||||
|
title,
|
||||||
|
display_name: None,
|
||||||
|
members: smallvec![],
|
||||||
|
new_messages: vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut pubkeys = vec![];
|
||||||
|
|
||||||
|
// Get all pubkeys from event's tags
|
||||||
|
pubkeys.extend(event.tags.public_keys().collect::<HashSet<_>>());
|
||||||
|
pubkeys.push(event.pubkey);
|
||||||
|
|
||||||
|
let client = get_client();
|
||||||
|
let (tx, rx) = oneshot::channel::<Vec<NostrProfile>>();
|
||||||
|
|
||||||
|
cx.background_spawn(async move {
|
||||||
|
let signer = client.signer().await.unwrap();
|
||||||
|
let signer_pubkey = signer.get_public_key().await.unwrap();
|
||||||
|
let mut profiles = vec![];
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if public_key == signer_pubkey {
|
||||||
|
profiles.push(profile);
|
||||||
|
} else {
|
||||||
|
profiles.insert(0, profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = tx.send(profiles);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
cx.spawn(|this, cx| async move {
|
||||||
|
if let Ok(profiles) = rx.await {
|
||||||
|
_ = cx.update(|cx| {
|
||||||
|
let display_name = if profiles.len() > 2 {
|
||||||
|
let merged = profiles
|
||||||
|
.iter()
|
||||||
|
.take(2)
|
||||||
|
.map(|profile| profile.name().to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
let name: SharedString =
|
||||||
|
format!("{}, +{}", merged, profiles.len() - 2).into();
|
||||||
|
|
||||||
|
Some(name)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = this.update(cx, |this: &mut Room, cx| {
|
||||||
|
this.members.extend(profiles);
|
||||||
|
this.display_name = display_name;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
this
|
||||||
|
});
|
||||||
|
|
||||||
|
room
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set contact's metadata by public key
|
pub fn id(&self) -> u64 {
|
||||||
pub fn set_metadata(&mut self, public_key: PublicKey, metadata: Metadata) {
|
self.id
|
||||||
if self.owner.public_key() == public_key {
|
|
||||||
self.owner.set_metadata(&metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
for member in self.members.iter_mut() {
|
|
||||||
if member.public_key() == public_key {
|
|
||||||
member.set_metadata(&metadata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get room's member by public key
|
/// Get room's member by public key
|
||||||
pub fn member(&self, public_key: &PublicKey) -> Option<NostrProfile> {
|
pub fn member(&self, public_key: &PublicKey) -> Option<NostrProfile> {
|
||||||
if &self.owner.public_key() == public_key {
|
self.members
|
||||||
Some(self.owner.clone())
|
.iter()
|
||||||
} else {
|
.find(|m| &m.public_key() == public_key)
|
||||||
self.members
|
.cloned()
|
||||||
.iter()
|
}
|
||||||
.find(|m| &m.public_key() == public_key)
|
|
||||||
.cloned()
|
/// Get room's first member's public key
|
||||||
}
|
pub fn first_member(&self) -> Option<&NostrProfile> {
|
||||||
|
self.members.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect room's member's public keys
|
||||||
|
pub fn public_keys(&self) -> Vec<PublicKey> {
|
||||||
|
self.members.iter().map(|m| m.public_key()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get room's display name
|
/// Get room's display name
|
||||||
pub fn name(&self) -> String {
|
pub fn name(&self) -> Option<SharedString> {
|
||||||
if self.members.len() <= 2 {
|
self.display_name.clone()
|
||||||
self.members
|
|
||||||
.iter()
|
|
||||||
.map(|profile| profile.name())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")
|
|
||||||
} else {
|
|
||||||
let name = self
|
|
||||||
.members
|
|
||||||
.iter()
|
|
||||||
.take(2)
|
|
||||||
.map(|profile| profile.name())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ");
|
|
||||||
|
|
||||||
format!("{}, +{}", name, self.members.len() - 2)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn last_seen(&self) -> &LastSeen {
|
/// Determine if room is a group
|
||||||
&self.last_seen
|
pub fn is_group(&self) -> bool {
|
||||||
|
self.members.len() > 2
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all public keys from current room
|
/// Get room's last seen
|
||||||
pub fn pubkeys(&self) -> Vec<PublicKey> {
|
pub fn last_seen(&self) -> Rc<LastSeen> {
|
||||||
let mut pubkeys: Vec<_> = self.members.iter().map(|m| m.public_key()).collect();
|
self.last_seen.clone()
|
||||||
pubkeys.push(self.owner.public_key());
|
}
|
||||||
|
|
||||||
pubkeys
|
/// Get room's last seen as ago format
|
||||||
|
pub fn ago(&self) -> SharedString {
|
||||||
|
self.last_seen.ago()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +1,54 @@
|
|||||||
use chrono::{Datelike, Local, TimeZone};
|
use chrono::{Local, TimeZone};
|
||||||
use gpui::SharedString;
|
use gpui::SharedString;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
|
const NOW: &str = "now";
|
||||||
|
const SECONDS_IN_MINUTE: i64 = 60;
|
||||||
|
const MINUTES_IN_HOUR: i64 = 60;
|
||||||
|
const HOURS_IN_DAY: i64 = 24;
|
||||||
|
const DAYS_IN_MONTH: i64 = 30;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct LastSeen(pub Timestamp);
|
pub struct LastSeen(pub Timestamp);
|
||||||
|
|
||||||
impl LastSeen {
|
impl LastSeen {
|
||||||
pub fn ago(&self) -> SharedString {
|
pub fn ago(&self) -> SharedString {
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
let input_time = Local.timestamp_opt(self.0.as_u64() as i64, 0).unwrap();
|
let input_time = match Local.timestamp_opt(self.0.as_u64() as i64, 0) {
|
||||||
let diff = (now - input_time).num_hours();
|
chrono::LocalResult::Single(time) => time,
|
||||||
|
_ => return "Invalid timestamp".into(),
|
||||||
|
};
|
||||||
|
let duration = now.signed_duration_since(input_time);
|
||||||
|
|
||||||
if diff < 24 {
|
match duration {
|
||||||
let duration = now.signed_duration_since(input_time);
|
d if d.num_seconds() < SECONDS_IN_MINUTE => NOW.into(),
|
||||||
|
d if d.num_minutes() < MINUTES_IN_HOUR => format!("{}m", d.num_minutes()),
|
||||||
if duration.num_seconds() < 60 {
|
d if d.num_hours() < HOURS_IN_DAY => format!("{}h", d.num_hours()),
|
||||||
"now".to_string().into()
|
d if d.num_days() < DAYS_IN_MONTH => format!("{}d", d.num_days()),
|
||||||
} else if duration.num_minutes() == 1 {
|
_ => input_time.format("%b %d").to_string(),
|
||||||
"1m".to_string().into()
|
|
||||||
} else if duration.num_minutes() < 60 {
|
|
||||||
format!("{}m", duration.num_minutes()).into()
|
|
||||||
} else if duration.num_hours() == 1 {
|
|
||||||
"1h".to_string().into()
|
|
||||||
} else if duration.num_hours() < 24 {
|
|
||||||
format!("{}h", duration.num_hours()).into()
|
|
||||||
} else if duration.num_days() == 1 {
|
|
||||||
"1d".to_string().into()
|
|
||||||
} else {
|
|
||||||
format!("{}d", duration.num_days()).into()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
input_time.format("%b %d").to_string().into()
|
|
||||||
}
|
}
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn human_readable(&self) -> SharedString {
|
pub fn human_readable(&self) -> SharedString {
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
let input_time = Local.timestamp_opt(self.0.as_u64() as i64, 0).unwrap();
|
let input_time = match Local.timestamp_opt(self.0.as_u64() as i64, 0) {
|
||||||
|
chrono::LocalResult::Single(time) => time,
|
||||||
|
_ => return "Invalid timestamp".into(),
|
||||||
|
};
|
||||||
|
|
||||||
if input_time.day() == now.day() {
|
let input_date = input_time.date_naive();
|
||||||
format!("Today at {}", input_time.format("%H:%M %p")).into()
|
let now_date = now.date_naive();
|
||||||
} else if input_time.day() == now.day() - 1 {
|
let yesterday_date = (now - chrono::Duration::days(1)).date_naive();
|
||||||
format!("Yesterday at {}", input_time.format("%H:%M %p")).into()
|
|
||||||
} else {
|
let time_format = input_time.format("%H:%M %p");
|
||||||
format!(
|
|
||||||
"{}, {}",
|
match input_date {
|
||||||
input_time.format("%d/%m/%y"),
|
date if date == now_date => format!("Today at {time_format}"),
|
||||||
input_time.format("%H:%M %p")
|
date if date == yesterday_date => format!("Yesterday at {time_format}"),
|
||||||
)
|
_ => format!("{}, {time_format}", input_time.format("%d/%m/%y")),
|
||||||
.into()
|
|
||||||
}
|
}
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set(&mut self, created_at: Timestamp) {
|
pub fn set(&mut self, created_at: Timestamp) {
|
||||||
|
|||||||
@@ -1,37 +1,24 @@
|
|||||||
use crate::constants::IMAGE_SERVICE;
|
use gpui::SharedString;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
use crate::constants::IMAGE_SERVICE;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct NostrProfile {
|
pub struct NostrProfile {
|
||||||
public_key: PublicKey,
|
public_key: PublicKey,
|
||||||
metadata: Metadata,
|
avatar: SharedString,
|
||||||
}
|
name: SharedString,
|
||||||
|
|
||||||
impl AsRef<PublicKey> for NostrProfile {
|
|
||||||
fn as_ref(&self) -> &PublicKey {
|
|
||||||
&self.public_key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsRef<Metadata> for NostrProfile {
|
|
||||||
fn as_ref(&self) -> &Metadata {
|
|
||||||
&self.metadata
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for NostrProfile {}
|
|
||||||
|
|
||||||
impl PartialEq for NostrProfile {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.public_key() == other.public_key()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NostrProfile {
|
impl NostrProfile {
|
||||||
pub fn new(public_key: PublicKey, metadata: Metadata) -> Self {
|
pub fn new(public_key: PublicKey, metadata: Metadata) -> Self {
|
||||||
|
let name = Self::extract_name(&public_key, &metadata);
|
||||||
|
let avatar = Self::extract_avatar(&metadata);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
public_key,
|
public_key,
|
||||||
metadata,
|
name,
|
||||||
|
avatar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,47 +27,44 @@ impl NostrProfile {
|
|||||||
self.public_key
|
self.public_key
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get contact's avatar
|
pub fn avatar(&self) -> SharedString {
|
||||||
pub fn avatar(&self) -> String {
|
self.avatar.clone()
|
||||||
if let Some(picture) = &self.metadata.picture {
|
}
|
||||||
if picture.len() > 1 {
|
|
||||||
|
pub fn name(&self) -> SharedString {
|
||||||
|
self.name.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_avatar(metadata: &Metadata) -> SharedString {
|
||||||
|
metadata
|
||||||
|
.picture
|
||||||
|
.as_ref()
|
||||||
|
.filter(|picture| !picture.is_empty())
|
||||||
|
.map(|picture| {
|
||||||
format!(
|
format!(
|
||||||
"{}/?url={}&w=100&h=100&fit=cover&mask=circle&n=-1",
|
"{}/?url={}&w=100&h=100&fit=cover&mask=circle&n=-1",
|
||||||
IMAGE_SERVICE, picture
|
IMAGE_SERVICE, picture
|
||||||
)
|
)
|
||||||
} else {
|
.into()
|
||||||
"brand/avatar.png".into()
|
})
|
||||||
}
|
.unwrap_or_else(|| "brand/avatar.png".into())
|
||||||
} else {
|
|
||||||
"brand/avatar.png".into()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get contact's name, fallback to public key as shorted format
|
fn extract_name(public_key: &PublicKey, metadata: &Metadata) -> SharedString {
|
||||||
pub fn name(&self) -> String {
|
if let Some(display_name) = metadata.display_name.as_ref() {
|
||||||
if let Some(display_name) = &self.metadata.display_name {
|
|
||||||
if !display_name.is_empty() {
|
if !display_name.is_empty() {
|
||||||
return display_name.to_owned();
|
return display_name.into();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(name) = &self.metadata.name {
|
if let Some(name) = metadata.name.as_ref() {
|
||||||
if !name.is_empty() {
|
if !name.is_empty() {
|
||||||
return name.to_owned();
|
return name.into();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let pubkey = self.public_key.to_string();
|
let pubkey = public_key.to_hex();
|
||||||
format!("{}:{}", &pubkey[0..4], &pubkey[pubkey.len() - 4..])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get contact's metadata
|
format!("{}:{}", &pubkey[0..4], &pubkey[pubkey.len() - 4..]).into()
|
||||||
pub fn metadata(&mut self) -> &Metadata {
|
|
||||||
&self.metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set contact's metadata
|
|
||||||
pub fn set_metadata(&mut self, metadata: &Metadata) {
|
|
||||||
self.metadata = metadata.clone()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +1,12 @@
|
|||||||
use crate::constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID, NIP96_SERVER};
|
use crate::constants::NIP96_SERVER;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use rnglib::{Language, RNG};
|
use rnglib::{Language, RNG};
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
collections::HashSet,
|
||||||
hash::{DefaultHasher, Hash, Hasher},
|
hash::{DefaultHasher, Hash, Hasher},
|
||||||
time::Duration,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn signer_public_key(client: &Client) -> anyhow::Result<PublicKey, anyhow::Error> {
|
|
||||||
let signer = client.signer().await?;
|
|
||||||
let public_key = signer.get_public_key().await?;
|
|
||||||
|
|
||||||
Ok(public_key)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn preload(client: &Client, public_key: PublicKey) -> anyhow::Result<(), anyhow::Error> {
|
|
||||||
let sync_opts = SyncOptions::default();
|
|
||||||
let subscription = Filter::new()
|
|
||||||
.kind(Kind::ContactList)
|
|
||||||
.author(public_key)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
// Get contact list
|
|
||||||
_ = client.sync(subscription, &sync_opts).await;
|
|
||||||
|
|
||||||
let all_messages_sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
|
|
||||||
let new_message_sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
|
||||||
|
|
||||||
// Create a filter for getting all gift wrapped events send to current user
|
|
||||||
let all_messages = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
|
||||||
|
|
||||||
// Create a filter for getting new message
|
|
||||||
let new_message = Filter::new()
|
|
||||||
.kind(Kind::GiftWrap)
|
|
||||||
.pubkey(public_key)
|
|
||||||
.limit(0);
|
|
||||||
|
|
||||||
// Subscribe for all messages
|
|
||||||
_ = client
|
|
||||||
.subscribe_with_id(
|
|
||||||
all_messages_sub_id,
|
|
||||||
all_messages,
|
|
||||||
Some(
|
|
||||||
SubscribeAutoCloseOptions::default()
|
|
||||||
.exit_policy(ReqExitPolicy::WaitDurationAfterEOSE(Duration::from_secs(3))),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Subscribe for new message
|
|
||||||
_ = client
|
|
||||||
.subscribe_with_id(new_message_sub_id, new_message, None)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn nip96_upload(client: &Client, file: Vec<u8>) -> anyhow::Result<Url, anyhow::Error> {
|
pub async fn nip96_upload(client: &Client, file: Vec<u8>) -> anyhow::Result<Url, anyhow::Error> {
|
||||||
let signer = client.signer().await?;
|
let signer = client.signer().await?;
|
||||||
let server_url = Url::parse(NIP96_SERVER)?;
|
let server_url = Url::parse(NIP96_SERVER)?;
|
||||||
@@ -68,10 +18,28 @@ pub async fn nip96_upload(client: &Client, file: Vec<u8>) -> anyhow::Result<Url,
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn room_hash(event: &Event) -> u64 {
|
pub fn room_hash(event: &Event) -> u64 {
|
||||||
let pubkeys: Vec<&PublicKey> = event.tags.public_keys().unique().collect();
|
|
||||||
let mut hasher = DefaultHasher::new();
|
let mut hasher = DefaultHasher::new();
|
||||||
|
let mut pubkeys: Vec<&PublicKey> = vec![];
|
||||||
|
|
||||||
|
// Add all public keys from event
|
||||||
|
pubkeys.push(&event.pubkey);
|
||||||
|
pubkeys.extend(
|
||||||
|
event
|
||||||
|
.tags
|
||||||
|
.public_keys()
|
||||||
|
.unique()
|
||||||
|
.sorted()
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
);
|
||||||
|
|
||||||
// Generate unique hash
|
// Generate unique hash
|
||||||
pubkeys.hash(&mut hasher);
|
pubkeys
|
||||||
|
.into_iter()
|
||||||
|
.unique()
|
||||||
|
.sorted()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.hash(&mut hasher);
|
||||||
|
|
||||||
hasher.finish()
|
hasher.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -358,8 +358,6 @@ impl Render for Dock {
|
|||||||
return div();
|
return div();
|
||||||
}
|
}
|
||||||
|
|
||||||
let cache_style = gpui::StyleRefinement::default().v_flex().size_full();
|
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.relative()
|
.relative()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
@@ -375,7 +373,7 @@ impl Render for Dock {
|
|||||||
.map(|this| match &self.panel {
|
.map(|this| match &self.panel {
|
||||||
DockItem::Split { view, .. } => this.child(view.clone()),
|
DockItem::Split { view, .. } => this.child(view.clone()),
|
||||||
DockItem::Tabs { view, .. } => this.child(view.clone()),
|
DockItem::Tabs { view, .. } => this.child(view.clone()),
|
||||||
DockItem::Panel { view, .. } => this.child(view.clone().view().cached(cache_style)),
|
DockItem::Panel { view, .. } => this.child(view.clone().view()),
|
||||||
})
|
})
|
||||||
.child(self.render_resize_handle(window, cx))
|
.child(self.render_resize_handle(window, cx))
|
||||||
.child(DockElement {
|
.child(DockElement {
|
||||||
|
|||||||
Reference in New Issue
Block a user