11 Commits
Author SHA1 Message Date
reya 5bef1a2c6c chore: fix flatpak 2025-08-10 14:58:59 +07:00
reya cd26244538 chore: bump version 2025-08-10 12:50:52 +07:00
reyaandGitHub ca622d1262 chore: improve logout behavior (#118)
* resubscribe on logout

* .

* .
2025-08-10 10:43:28 +07:00
reyaandGitHub 5011becacb chore: use a newer flatpak runtime (#117)
* use newer flatpak runtime

* .
2025-08-10 07:40:13 +07:00
reyaandGitHub 17f92d767e chore: improve ui consistency (#115)
* .

* .
2025-08-09 14:58:01 +07:00
reya be660cb14b chore: update deps 2025-08-08 13:14:11 +07:00
reyaandGitHub 8fca202c05 chore: refactor subscription (#113)
* fix duplicate set signer request

* refactor

* .
2025-08-07 20:53:21 +07:00
reya 7b20131e3b chore: bump version 2025-08-06 10:11:04 +07:00
reyaandGitHub 9127696517 chore: fix missing message during initial fetch (#110)
* remove fetched flag

* .

* improve
2025-08-06 09:15:00 +07:00
reyaandGitHub af74a4ed23 . (#109) 2025-08-06 07:10:00 +07:00
reyaandGitHub bd2b72a57a chore: fix duplicate messages (#108)
* prevent duplicate message on load

* refactor
2025-08-05 21:15:10 +07:00
20 changed files with 609 additions and 755 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
rustup: [stable, nightly] rustup: [stable]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
Generated
+77 -75
View File
@@ -178,7 +178,7 @@ dependencies = [
[[package]] [[package]]
name = "assets" name = "assets"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"gpui", "gpui",
@@ -233,7 +233,7 @@ dependencies = [
"async-task", "async-task",
"concurrent-queue", "concurrent-queue",
"fastrand 2.3.0", "fastrand 2.3.0",
"futures-lite 2.6.0", "futures-lite 2.6.1",
"pin-project-lite", "pin-project-lite",
"slab", "slab",
] ]
@@ -246,7 +246,7 @@ checksum = "09f7e37c0ed80b2a977691c47dae8625cfb21e205827106c64f7c588766b2e50"
dependencies = [ dependencies = [
"async-lock", "async-lock",
"blocking", "blocking",
"futures-lite 2.6.0", "futures-lite 2.6.1",
] ]
[[package]] [[package]]
@@ -259,7 +259,7 @@ dependencies = [
"cfg-if", "cfg-if",
"concurrent-queue", "concurrent-queue",
"futures-io", "futures-io",
"futures-lite 2.6.0", "futures-lite 2.6.1",
"parking", "parking",
"polling", "polling",
"rustix 1.0.8", "rustix 1.0.8",
@@ -286,7 +286,7 @@ checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
dependencies = [ dependencies = [
"async-io", "async-io",
"blocking", "blocking",
"futures-lite 2.6.0", "futures-lite 2.6.1",
] ]
[[package]] [[package]]
@@ -303,7 +303,7 @@ dependencies = [
"blocking", "blocking",
"cfg-if", "cfg-if",
"event-listener", "event-listener",
"futures-lite 2.6.0", "futures-lite 2.6.1",
"rustix 1.0.8", "rustix 1.0.8",
] ]
@@ -392,7 +392,7 @@ checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52"
dependencies = [ dependencies = [
"async-compression", "async-compression",
"crc32fast", "crc32fast",
"futures-lite 2.6.0", "futures-lite 2.6.1",
"pin-project", "pin-project",
"thiserror 1.0.69", "thiserror 1.0.69",
] ]
@@ -417,7 +417,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]] [[package]]
name = "auto_update" name = "auto_update"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cargo-packager-updater", "cargo-packager-updater",
@@ -749,7 +749,7 @@ dependencies = [
"async-channel", "async-channel",
"async-task", "async-task",
"futures-io", "futures-io",
"futures-lite 2.6.0", "futures-lite 2.6.1",
"piper", "piper",
] ]
@@ -777,18 +777,18 @@ checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]] [[package]]
name = "bytemuck" name = "bytemuck"
version = "1.23.1" version = "1.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677"
dependencies = [ dependencies = [
"bytemuck_derive", "bytemuck_derive",
] ]
[[package]] [[package]]
name = "bytemuck_derive" name = "bytemuck_derive"
version = "1.10.0" version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "441473f2b4b0459a68628c744bc61d23e730fb00128b841d30fa4bb3972257e4" checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -904,9 +904,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.31" version = "1.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2" checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
@@ -1021,7 +1021,7 @@ dependencies = [
[[package]] [[package]]
name = "client_keys" name = "client_keys"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"global", "global",
@@ -1123,7 +1123,7 @@ dependencies = [
[[package]] [[package]]
name = "collections" name = "collections"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ea7c3a23fb2eb0f2c5d811eec3337897886482ee" source = "git+https://github.com/zed-industries/zed#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
@@ -1158,7 +1158,7 @@ dependencies = [
[[package]] [[package]]
name = "common" name = "common"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -1214,7 +1214,7 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]] [[package]]
name = "coop" name = "coop"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assets", "assets",
@@ -1544,7 +1544,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#ea7c3a23fb2eb0f2c5d811eec3337897886482ee" source = "git+https://github.com/zed-industries/zed#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1718,7 +1718,7 @@ dependencies = [
"cc", "cc",
"memchr", "memchr",
"rustc_version", "rustc_version",
"toml 0.9.4", "toml 0.9.5",
"vswhom", "vswhom",
"winreg", "winreg",
] ]
@@ -2210,9 +2210,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-lite" name = "futures-lite"
version = "2.6.0" version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
dependencies = [ dependencies = [
"fastrand 2.3.0", "fastrand 2.3.0",
"futures-core", "futures-core",
@@ -2342,7 +2342,7 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]] [[package]]
name = "global" name = "global"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"dirs 5.0.1", "dirs 5.0.1",
@@ -2436,7 +2436,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui" name = "gpui"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ea7c3a23fb2eb0f2c5d811eec3337897886482ee" source = "git+https://github.com/zed-industries/zed#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"as-raw-xcb-connection", "as-raw-xcb-connection",
@@ -2529,7 +2529,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#ea7c3a23fb2eb0f2c5d811eec3337897886482ee" source = "git+https://github.com/zed-industries/zed#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
dependencies = [ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
@@ -2541,7 +2541,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_tokio" name = "gpui_tokio"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ea7c3a23fb2eb0f2c5d811eec3337897886482ee" source = "git+https://github.com/zed-industries/zed#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
dependencies = [ dependencies = [
"gpui", "gpui",
"tokio", "tokio",
@@ -2551,15 +2551,15 @@ dependencies = [
[[package]] [[package]]
name = "grid" name = "grid"
version = "0.17.0" version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71b01d27060ad58be4663b9e4ac9e2d4806918e8876af8912afbddd1a91d5eaa" checksum = "12101ecc8225ea6d675bc70263074eab6169079621c2186fe0c66590b2df9681"
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.4.11" version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
dependencies = [ dependencies = [
"atomic-waker", "atomic-waker",
"bytes", "bytes",
@@ -2602,9 +2602,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.4" version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [ dependencies = [
"foldhash", "foldhash",
] ]
@@ -2772,7 +2772,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#ea7c3a23fb2eb0f2c5d811eec3337897886482ee" source = "git+https://github.com/zed-industries/zed#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -2782,6 +2782,7 @@ dependencies = [
"http-body", "http-body",
"log", "log",
"parking_lot", "parking_lot",
"reqwest 0.12.15",
"serde", "serde",
"serde_json", "serde_json",
"url", "url",
@@ -2791,7 +2792,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client_tls" name = "http_client_tls"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ea7c3a23fb2eb0f2c5d811eec3337897886482ee" source = "git+https://github.com/zed-industries/zed#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
dependencies = [ dependencies = [
"rustls", "rustls",
"rustls-platform-verifier", "rustls-platform-verifier",
@@ -2885,7 +2886,7 @@ dependencies = [
[[package]] [[package]]
name = "i18n" name = "i18n"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"rust-i18n", "rust-i18n",
] ]
@@ -3002,7 +3003,7 @@ dependencies = [
[[package]] [[package]]
name = "identity" name = "identity"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"client_keys", "client_keys",
@@ -3107,7 +3108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.15.4", "hashbrown 0.15.5",
"serde", "serde",
] ]
@@ -3593,7 +3594,7 @@ dependencies = [
[[package]] [[package]]
name = "media" name = "media"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ea7c3a23fb2eb0f2c5d811eec3337897886482ee" source = "git+https://github.com/zed-industries/zed#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bindgen 0.71.1", "bindgen 0.71.1",
@@ -3721,7 +3722,7 @@ dependencies = [
"cfg_aliases", "cfg_aliases",
"codespan-reporting 0.12.0", "codespan-reporting 0.12.0",
"half", "half",
"hashbrown 0.15.4", "hashbrown 0.15.5",
"hexf-parse", "hexf-parse",
"indexmap", "indexmap",
"log", "log",
@@ -3831,7 +3832,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr" name = "nostr"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#9d91709c35f743361d68c16349d33979329ebd84" source = "git+https://github.com/rust-nostr/nostr#0ac295f66052dc822f425a53daaa16101a8ef3bd"
dependencies = [ dependencies = [
"aes", "aes",
"base64", "base64",
@@ -3854,7 +3855,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-connect" name = "nostr-connect"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#9d91709c35f743361d68c16349d33979329ebd84" source = "git+https://github.com/rust-nostr/nostr#0ac295f66052dc822f425a53daaa16101a8ef3bd"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -3866,7 +3867,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-database" name = "nostr-database"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#9d91709c35f743361d68c16349d33979329ebd84" source = "git+https://github.com/rust-nostr/nostr#0ac295f66052dc822f425a53daaa16101a8ef3bd"
dependencies = [ dependencies = [
"flatbuffers", "flatbuffers",
"lru", "lru",
@@ -3877,7 +3878,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-lmdb" name = "nostr-lmdb"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#9d91709c35f743361d68c16349d33979329ebd84" source = "git+https://github.com/rust-nostr/nostr#0ac295f66052dc822f425a53daaa16101a8ef3bd"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"flume", "flume",
@@ -3891,7 +3892,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-relay-pool" name = "nostr-relay-pool"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#9d91709c35f743361d68c16349d33979329ebd84" source = "git+https://github.com/rust-nostr/nostr#0ac295f66052dc822f425a53daaa16101a8ef3bd"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"async-wsocket", "async-wsocket",
@@ -3907,7 +3908,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-sdk" name = "nostr-sdk"
version = "0.43.0" version = "0.43.0"
source = "git+https://github.com/rust-nostr/nostr#9d91709c35f743361d68c16349d33979329ebd84" source = "git+https://github.com/rust-nostr/nostr#0ac295f66052dc822f425a53daaa16101a8ef3bd"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -4219,7 +4220,7 @@ dependencies = [
"cipher", "cipher",
"digest", "digest",
"endi", "endi",
"futures-lite 2.6.0", "futures-lite 2.6.1",
"futures-util", "futures-util",
"getrandom 0.3.3", "getrandom 0.3.3",
"hkdf", "hkdf",
@@ -4983,7 +4984,7 @@ dependencies = [
[[package]] [[package]]
name = "refineable" name = "refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ea7c3a23fb2eb0f2c5d811eec3337897886482ee" source = "git+https://github.com/zed-industries/zed#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
dependencies = [ dependencies = [
"derive_refineable", "derive_refineable",
"workspace-hack", "workspace-hack",
@@ -5020,7 +5021,7 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]] [[package]]
name = "registry" name = "registry"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@@ -5061,6 +5062,7 @@ dependencies = [
"js-sys", "js-sys",
"log", "log",
"mime", "mime",
"mime_guess",
"once_cell", "once_cell",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
@@ -5140,7 +5142,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#ea7c3a23fb2eb0f2c5d811eec3337897886482ee" source = "git+https://github.com/zed-industries/zed#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -5376,7 +5378,7 @@ dependencies = [
"openssl-probe", "openssl-probe",
"rustls-pki-types", "rustls-pki-types",
"schannel", "schannel",
"security-framework 3.2.0", "security-framework 3.3.0",
] ]
[[package]] [[package]]
@@ -5413,7 +5415,7 @@ dependencies = [
"rustls-native-certs", "rustls-native-certs",
"rustls-platform-verifier-android", "rustls-platform-verifier-android",
"rustls-webpki", "rustls-webpki",
"security-framework 3.2.0", "security-framework 3.3.0",
"security-framework-sys", "security-framework-sys",
"webpki-root-certs 0.26.11", "webpki-root-certs 0.26.11",
"windows-sys 0.59.0", "windows-sys 0.59.0",
@@ -5439,9 +5441,9 @@ dependencies = [
[[package]] [[package]]
name = "rustversion" name = "rustversion"
version = "1.0.21" version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]] [[package]]
name = "rustybuzz" name = "rustybuzz"
@@ -5646,9 +5648,9 @@ dependencies = [
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "3.2.0" version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c"
dependencies = [ dependencies = [
"bitflags 2.9.1", "bitflags 2.9.1",
"core-foundation 0.10.1", "core-foundation 0.10.1",
@@ -5676,7 +5678,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
[[package]] [[package]]
name = "semantic_version" name = "semantic_version"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ea7c3a23fb2eb0f2c5d811eec3337897886482ee" source = "git+https://github.com/zed-industries/zed#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"serde", "serde",
@@ -5811,7 +5813,7 @@ dependencies = [
[[package]] [[package]]
name = "settings" name = "settings"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"global", "global",
@@ -5918,9 +5920,9 @@ dependencies = [
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.10" version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
[[package]] [[package]]
name = "slotmap" name = "slotmap"
@@ -5951,7 +5953,7 @@ dependencies = [
"async-net", "async-net",
"async-process", "async-process",
"blocking", "blocking",
"futures-lite 2.6.0", "futures-lite 2.6.1",
] ]
[[package]] [[package]]
@@ -6071,7 +6073,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#ea7c3a23fb2eb0f2c5d811eec3337897886482ee" source = "git+https://github.com/zed-industries/zed#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"log", "log",
@@ -6294,9 +6296,9 @@ dependencies = [
[[package]] [[package]]
name = "taffy" name = "taffy"
version = "0.8.3" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7aaef0ac998e6527d6d0d5582f7e43953bb17221ac75bb8eb2fcc2db3396db1c" checksum = "a13e5d13f79d558b5d353a98072ca8ca0e99da429467804de959aa8c83c9a004"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"grid", "grid",
@@ -6374,7 +6376,7 @@ dependencies = [
[[package]] [[package]]
name = "theme" name = "theme"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"gpui", "gpui",
@@ -6534,7 +6536,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "title_bar" name = "title_bar"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"common", "common",
@@ -6655,9 +6657,9 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.9.4" version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41ae868b5a0f67631c14589f7e250c1ea2c574ee5ba21c6c8dd4b1485705a5a1" checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
@@ -6702,9 +6704,9 @@ dependencies = [
[[package]] [[package]]
name = "toml_parser" name = "toml_parser"
version = "1.0.1" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10"
dependencies = [ dependencies = [
"winnow", "winnow",
] ]
@@ -6905,7 +6907,7 @@ dependencies = [
[[package]] [[package]]
name = "ui" name = "ui"
version = "0.2.0" version = "0.2.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"common", "common",
@@ -7105,7 +7107,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]] [[package]]
name = "util" name = "util"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#ea7c3a23fb2eb0f2c5d811eec3337897886482ee" source = "git+https://github.com/zed-industries/zed#2d9cd2ac8888a144ef41e59c9820ffbecee66ed1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-fs", "async-fs",
@@ -8312,7 +8314,7 @@ dependencies = [
"enumflags2", "enumflags2",
"event-listener", "event-listener",
"futures-core", "futures-core",
"futures-lite 2.6.0", "futures-lite 2.6.1",
"hex", "hex",
"nix 0.30.1", "nix 0.30.1",
"ordered-stream", "ordered-stream",
@@ -8434,9 +8436,9 @@ dependencies = [
[[package]] [[package]]
name = "zerovec" name = "zerovec"
version = "0.11.2" version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
dependencies = [ dependencies = [
"yoke", "yoke",
"zerofrom", "zerofrom",
+2 -2
View File
@@ -4,12 +4,12 @@ members = ["crates/*"]
default-members = ["crates/coop"] default-members = ["crates/coop"]
[workspace.package] [workspace.package]
version = "0.2.0" version = "0.2.2"
edition = "2021" edition = "2021"
publish = false publish = false
[workspace.metadata.i18n] [workspace.metadata.i18n]
available-locales = ["en", "zh-CN", "zh-TW", "ru", "vi", "ja", "es", "pt", "ko"] available-locales = ["en"]
default-locale = "en" default-locale = "en"
load-path = "locales" load-path = "locales"
+1 -1
View File
@@ -14,7 +14,7 @@ product-name = "Coop"
description = "Chat Freely, Stay Private on Nostr" description = "Chat Freely, Stay Private on Nostr"
identifier = "su.reya.coop" identifier = "su.reya.coop"
category = "SocialNetworking" category = "SocialNetworking"
version = "0.2.0" version = "0.2.2"
out-dir = "../../dist" out-dir = "../../dist"
before-packaging-command = "cargo build --release" before-packaging-command = "cargo build --release"
resources = ["Cargo.toml", "src"] resources = ["Cargo.toml", "src"]
@@ -1,7 +1,7 @@
{ {
"id": "$APP_ID", "id": "$APP_ID",
"runtime": "org.freedesktop.Platform", "runtime": "org.freedesktop.Platform",
"runtime-version": "23.08", "runtime-version": "24.08",
"sdk": "org.freedesktop.Sdk", "sdk": "org.freedesktop.Sdk",
"sdk-extensions": ["org.freedesktop.Sdk.Extension.rust-stable"], "sdk-extensions": ["org.freedesktop.Sdk.Extension.rust-stable"],
"command": "coop", "command": "coop",
+85 -54
View File
@@ -27,8 +27,10 @@ use ui::button::{Button, ButtonVariants};
use ui::dock_area::dock::DockPlacement; use ui::dock_area::dock::DockPlacement;
use ui::dock_area::panel::PanelView; use ui::dock_area::panel::PanelView;
use ui::dock_area::{ClosePanel, DockArea, DockItem}; use ui::dock_area::{ClosePanel, DockArea, DockItem};
use ui::indicator::Indicator;
use ui::modal::ModalButtonProps; use ui::modal::ModalButtonProps;
use ui::popup_menu::PopupMenuExt; use ui::popup_menu::PopupMenuExt;
use ui::tooltip::Tooltip;
use ui::{h_flex, ContextModal, IconName, Root, Sizable, StyledExt}; use ui::{h_flex, ContextModal, IconName, Root, Sizable, StyledExt};
use crate::views::compose::compose_button; use crate::views::compose::compose_button;
@@ -109,55 +111,9 @@ impl ChatSpace {
subscriptions.push(cx.observe_in( subscriptions.push(cx.observe_in(
&client_keys, &client_keys,
window, window,
|_this: &mut Self, state, window, cx| { |this: &mut Self, state, window, cx| {
if !state.read(cx).has_keys() { if !state.read(cx).has_keys() {
let title = SharedString::new(t!("startup.client_keys_warning")); this.render_client_keys_modal(window, cx);
let desc = SharedString::new(t!("startup.client_keys_desc"));
window.open_modal(cx, move |this, _window, cx| {
this.overlay_closable(false)
.show_close(false)
.keyboard(false)
.confirm()
.button_props(
ModalButtonProps::default()
.cancel_text(t!("startup.create_new_keys"))
.ok_text(t!("common.allow")),
)
.child(
div()
.w_full()
.h_40()
.flex()
.flex_col()
.gap_1()
.items_center()
.justify_center()
.text_center()
.text_sm()
.child(
div()
.font_semibold()
.text_color(cx.theme().text_muted)
.child(title.clone()),
)
.child(desc.clone()),
)
.on_cancel(|_, _window, cx| {
ClientKeys::global(cx).update(cx, |this, cx| {
this.new_keys(cx);
});
// true: Close modal
true
})
.on_ok(|_, window, cx| {
ClientKeys::global(cx).update(cx, |this, cx| {
this.load(window, cx);
});
// true: Close modal
true
})
});
} }
}, },
)); ));
@@ -299,8 +255,13 @@ impl ChatSpace {
} }
fn on_sign_out(&mut self, _ev: &Logout, window: &mut Window, cx: &mut Context<Self>) { fn on_sign_out(&mut self, _ev: &Logout, window: &mut Window, cx: &mut Context<Self>) {
let registry = Registry::global(cx);
let identity = Identity::global(cx); let identity = Identity::global(cx);
// TODO: save current session?
registry.update(cx, |this, cx| {
this.reset(cx);
});
identity.update(cx, |this, cx| { identity.update(cx, |this, cx| {
this.unload(window, cx); this.unload(window, cx);
}); });
@@ -324,14 +285,84 @@ impl ChatSpace {
}); });
} }
fn render_client_keys_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let title = SharedString::new(t!("startup.client_keys_warning"));
let desc = SharedString::new(t!("startup.client_keys_desc"));
window.open_modal(cx, move |this, _window, cx| {
this.overlay_closable(false)
.show_close(false)
.keyboard(false)
.confirm()
.button_props(
ModalButtonProps::default()
.cancel_text(t!("startup.create_new_keys"))
.ok_text(t!("common.allow")),
)
.child(
div()
.w_full()
.h_40()
.flex()
.flex_col()
.gap_1()
.items_center()
.justify_center()
.text_center()
.text_sm()
.child(
div()
.font_semibold()
.text_color(cx.theme().text_muted)
.child(title.clone()),
)
.child(desc.clone()),
)
.on_cancel(|_, _window, cx| {
ClientKeys::global(cx).update(cx, |this, cx| {
this.new_keys(cx);
});
// true: Close modal
true
})
.on_ok(|_, window, cx| {
ClientKeys::global(cx).update(cx, |this, cx| {
this.load(window, cx);
});
// true: Close modal
true
})
});
}
fn render_titlebar_left_side( fn render_titlebar_left_side(
&mut self, &mut self,
_window: &mut Window, _window: &mut Window,
_cx: &Context<Self>, cx: &Context<Self>,
) -> impl IntoElement { ) -> impl IntoElement {
let compose_button = compose_button().into_any_element(); let registry = Registry::read_global(cx);
let loading = registry.loading;
h_flex().gap_1().child(compose_button) h_flex()
.gap_2()
.child(compose_button())
.when(loading, |this| {
this.child(
h_flex()
.id("downloading")
.px_4()
.h_6()
.gap_1()
.text_xs()
.rounded_full()
.bg(cx.theme().elevated_surface_background)
.child(shared_t!("loading.label"))
.child(Indicator::new().xsmall())
.tooltip(|window, cx| {
Tooltip::new(t!("loading.tooltip"), window, cx).into()
}),
)
})
} }
fn render_titlebar_right_side( fn render_titlebar_right_side(
@@ -342,7 +373,7 @@ impl ChatSpace {
) -> impl IntoElement { ) -> impl IntoElement {
let proxy = AppSettings::get_proxy_user_avatars(cx); let proxy = AppSettings::get_proxy_user_avatars(cx);
let need_backup = Identity::read_global(cx).need_backup(); let need_backup = Identity::read_global(cx).need_backup();
let relay_ready = Identity::read_global(cx).relay_ready(); let has_dm_relays = Identity::read_global(cx).has_dm_relays();
let updating = AutoUpdater::read_global(cx).status.is_updating(); let updating = AutoUpdater::read_global(cx).status.is_updating();
let updated = AutoUpdater::read_global(cx).status.is_updated(); let updated = AutoUpdater::read_global(cx).status.is_updated();
@@ -377,7 +408,7 @@ impl ChatSpace {
}), }),
) )
}) })
.when_some(relay_ready, |this, status| { .when_some(has_dm_relays, |this, status| {
this.when(!status, |this| this.child(messaging_relays::relay_button())) this.when(!status, |this| this.child(messaging_relays::relay_button()))
}) })
.when_some(need_backup, |this, keys| { .when_some(need_backup, |this, keys| {
+116 -113
View File
@@ -6,10 +6,10 @@ use anyhow::{anyhow, Error};
use assets::Assets; use assets::Assets;
use common::event::EventUtils; use common::event::EventUtils;
use global::constants::{ use global::constants::{
ALL_MESSAGES_ID, APP_ID, APP_NAME, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT, APP_ID, APP_NAME, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT, METADATA_BATCH_TIMEOUT,
METADATA_BATCH_TIMEOUT, NEW_MESSAGE_ID, SEARCH_RELAYS, SEARCH_RELAYS, WAIT_FOR_FINISH,
}; };
use global::{nostr_client, set_all_gift_wraps_fetched, NostrSignal}; use global::{nostr_client, processed_events, starting_time, NostrSignal};
use gpui::{ use gpui::{
actions, point, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, actions, point, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem,
SharedString, TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations, SharedString, TitlebarOptions, WindowBackgroundAppearance, WindowBounds, WindowDecorations,
@@ -39,6 +39,9 @@ fn main() {
// Initialize the Nostr Client // Initialize the Nostr Client
let client = nostr_client(); let client = nostr_client();
// Initialize the starting time
let _ = starting_time();
// Initialize the Application // Initialize the Application
let app = Application::new() let app = Application::new()
.with_assets(Assets) .with_assets(Assets)
@@ -46,10 +49,8 @@ fn main() {
let (signal_tx, signal_rx) = channel::bounded::<NostrSignal>(2048); let (signal_tx, signal_rx) = channel::bounded::<NostrSignal>(2048);
let (mta_tx, mta_rx) = channel::bounded::<PublicKey>(1024); let (mta_tx, mta_rx) = channel::bounded::<PublicKey>(1024);
let (event_tx, event_rx) = channel::unbounded::<Event>(); let (event_tx, event_rx) = channel::bounded::<Event>(2048);
let signal_tx_clone = signal_tx.clone(); let signal_tx_clone = signal_tx.clone();
let mta_tx_clone = mta_tx.clone();
app.background_executor() app.background_executor()
.spawn(async move { .spawn(async move {
@@ -61,9 +62,7 @@ fn main() {
// Handle Nostr notifications. // Handle Nostr notifications.
// //
// Send the redefined signal back to GPUI via channel. // Send the redefined signal back to GPUI via channel.
if let Err(e) = if let Err(e) = handle_nostr_notifications(&signal_tx_clone, &event_tx).await {
handle_nostr_notifications(client, &signal_tx_clone, &mta_tx_clone, &event_tx).await
{
log::error!("Failed to handle Nostr notifications: {e}"); log::error!("Failed to handle Nostr notifications: {e}");
} }
}) })
@@ -72,6 +71,7 @@ fn main() {
app.background_executor() app.background_executor()
.spawn(async move { .spawn(async move {
let duration = Duration::from_millis(METADATA_BATCH_TIMEOUT); let duration = Duration::from_millis(METADATA_BATCH_TIMEOUT);
let mut processed_pubkeys: BTreeSet<PublicKey> = BTreeSet::new();
let mut batch: BTreeSet<PublicKey> = BTreeSet::new(); let mut batch: BTreeSet<PublicKey> = BTreeSet::new();
/// Internal events for the metadata batching system /// Internal events for the metadata batching system
@@ -99,20 +99,23 @@ fn main() {
match smol::future::or(recv(), timeout()).await { match smol::future::or(recv(), timeout()).await {
BatchEvent::NewKeys(public_key) => { BatchEvent::NewKeys(public_key) => {
batch.insert(public_key); // Prevent duplicate keys from being processed
// Process immediately if batch limit reached if processed_pubkeys.insert(public_key) {
batch.insert(public_key);
}
// Process the batch if it's full
if batch.len() >= METADATA_BATCH_LIMIT { if batch.len() >= METADATA_BATCH_LIMIT {
sync_data_for_pubkeys(client, std::mem::take(&mut batch)).await; sync_data_for_pubkeys(std::mem::take(&mut batch)).await;
} }
} }
BatchEvent::Timeout => { BatchEvent::Timeout => {
if !batch.is_empty() { if !batch.is_empty() {
sync_data_for_pubkeys(client, std::mem::take(&mut batch)).await; sync_data_for_pubkeys(std::mem::take(&mut batch)).await;
} }
} }
BatchEvent::Closed => { BatchEvent::Closed => {
if !batch.is_empty() { if !batch.is_empty() {
sync_data_for_pubkeys(client, std::mem::take(&mut batch)).await; sync_data_for_pubkeys(std::mem::take(&mut batch)).await;
} }
break; break;
} }
@@ -131,7 +134,7 @@ fn main() {
continue; continue;
} }
let duration = smol::Timer::after(Duration::from_secs(30)); let duration = smol::Timer::after(Duration::from_secs(WAIT_FOR_FINISH));
let recv = || async { let recv = || async {
// no inline // no inline
@@ -145,8 +148,7 @@ fn main() {
match smol::future::or(recv(), timeout()).await { match smol::future::or(recv(), timeout()).await {
Some(event) => { Some(event) => {
// Process the gift wrap event unwrapping let cached = try_unwrap_event(&event, &signal_tx, &mta_tx).await;
let cached = try_unwrap_event(&signal_tx, &mta_tx, &event, false).await;
// Increment the total messages counter if message is not from cache // Increment the total messages counter if message is not from cache
if !cached { if !cached {
@@ -163,17 +165,9 @@ fn main() {
None => { None => {
// Notify the UI that the processing is finished // Notify the UI that the processing is finished
signal_tx.send(NostrSignal::Finish).await.ok(); signal_tx.send(NostrSignal::Finish).await.ok();
// Mark all gift wraps as fetched
// For the next time Coop only needs to process new gift wraps
set_all_gift_wraps_fetched().await;
break;
} }
} }
} }
// Event channel is no longer needed when all gift wrap events have been processed
event_rx.close();
}) })
.detach(); .detach();
@@ -246,12 +240,10 @@ fn main() {
// Spawn a task to handle events from nostr channel // Spawn a task to handle events from nostr channel
cx.spawn_in(window, async move |_, cx| { cx.spawn_in(window, async move |_, cx| {
let all_messages = SubscriptionId::new(ALL_MESSAGES_ID);
while let Ok(signal) = signal_rx.recv().await { while let Ok(signal) = signal_rx.recv().await {
cx.update(|window, cx| { cx.update(|window, cx| {
let registry = Registry::global(cx); let registry = Registry::global(cx);
let identity = Identity::read_global(cx); let identity = Identity::global(cx);
match signal { match signal {
// Load chat rooms and stop the loading status // Load chat rooms and stop the loading status
@@ -275,15 +267,6 @@ fn main() {
} }
}); });
} }
// Load chat rooms without setting as finished
NostrSignal::Eose(subscription_id) => {
// Only load chat rooms if the subscription ID matches the all_messages_sub_id
if subscription_id == all_messages {
registry.update(cx, |this, cx| {
this.load_rooms(window, cx);
});
}
}
// Add the new metadata to the registry or update the existing one // Add the new metadata to the registry or update the existing one
NostrSignal::Metadata(event) => { NostrSignal::Metadata(event) => {
registry.update(cx, |this, cx| { registry.update(cx, |this, cx| {
@@ -292,12 +275,17 @@ fn main() {
} }
// Convert the gift wrapped message to a message // Convert the gift wrapped message to a message
NostrSignal::GiftWrap(event) => { NostrSignal::GiftWrap(event) => {
if let Some(public_key) = identity.public_key() { if let Some(public_key) = identity.read(cx).public_key() {
registry.update(cx, |this, cx| { registry.update(cx, |this, cx| {
this.event_to_message(public_key, event, window, cx); this.event_to_message(public_key, event, window, cx);
}); });
} }
} }
NostrSignal::DmRelaysFound => {
identity.update(cx, |this, cx| {
this.set_has_dm_relays(cx);
});
}
NostrSignal::Notice(_msg) => { NostrSignal::Notice(_msg) => {
// window.push_notification(msg, cx); // window.push_notification(msg, cx);
} }
@@ -364,83 +352,102 @@ async fn connect(client: &Client) -> Result<(), Error> {
} }
async fn handle_nostr_notifications( async fn handle_nostr_notifications(
client: &Client,
signal_tx: &Sender<NostrSignal>, signal_tx: &Sender<NostrSignal>,
mta_tx: &Sender<PublicKey>,
event_tx: &Sender<Event>, event_tx: &Sender<Event>,
) -> Result<(), Error> { ) -> Result<(), Error> {
let new_messages = SubscriptionId::new(NEW_MESSAGE_ID); let client = nostr_client();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let auto_close = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let mut notifications = client.notifications(); let mut notifications = client.notifications();
let mut processed_events: BTreeSet<EventId> = BTreeSet::new();
let mut processed_dm_relays: BTreeSet<PublicKey> = BTreeSet::new();
while let Ok(notification) = notifications.recv().await { while let Ok(notification) = notifications.recv().await {
let RelayPoolNotification::Message { message, .. } = notification else { let RelayPoolNotification::Message { message, .. } = notification else {
continue; continue;
}; };
match message { let RelayMessage::Event { event, .. } = message else {
RelayMessage::Event { continue;
event, };
subscription_id,
} => {
if processed_events.contains(&event.id) {
continue;
}
// Skip events that have already been processed
processed_events.insert(event.id);
match event.kind { // Skip events that have already been processed
Kind::GiftWrap => { if !processed_events().write().await.insert(event.id) {
if *subscription_id == new_messages { continue;
let event = event.as_ref(); }
_ = try_unwrap_event(signal_tx, mta_tx, event, false).await;
} else {
event_tx.send(event.into_owned()).await.ok();
}
}
Kind::Metadata => {
signal_tx
.send(NostrSignal::Metadata(event.into_owned()))
.await
.ok();
}
Kind::ContactList => {
if let Ok(true) = check_author(client, &event).await {
for public_key in event.tags.public_keys().copied() {
mta_tx.send(public_key).await.ok();
}
}
}
Kind::RelayList => {
if processed_dm_relays.contains(&event.pubkey) {
continue;
}
// Skip public keys that have already been processed
processed_dm_relays.insert(event.pubkey);
let filter = Filter::new() match event.kind {
.author(event.pubkey) Kind::RelayList => {
.kind(Kind::InboxRelays) // Get metadata for event's pubkey that matches the current user's pubkey
.limit(1); if let Ok(true) = is_from_current_user(&event).await {
let sub_id = SubscriptionId::new("metadata");
let filter = Filter::new()
.kinds(vec![Kind::Metadata, Kind::ContactList, Kind::InboxRelays])
.author(event.pubkey)
.limit(10);
if let Ok(output) = client.subscribe(filter, Some(opts)).await { client
log::info!( .subscribe_with_id(sub_id, filter, Some(auto_close))
"Subscribed to get DM relays: {} - Relays: {:?}", .await
event.pubkey.to_bech32().unwrap(), .ok();
output.success
)
}
}
_ => {}
} }
} }
RelayMessage::EndOfStoredEvents(subscription_id) => { Kind::InboxRelays => {
if let Ok(true) = is_from_current_user(&event).await {
// Get all inbox relays
let relays = event
.tags
.filter_standardized(TagKind::Relay)
.filter_map(|t| {
if let TagStandard::Relay(url) = t {
Some(url.to_owned())
} else {
None
}
})
.collect_vec();
if !relays.is_empty() {
// Add relays to nostr client
for relay in relays.iter() {
_ = client.add_relay(relay).await;
_ = client.connect_relay(relay).await;
}
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(event.pubkey);
let sub_id = SubscriptionId::new("gift-wrap");
// Notify the UI that the current user has set up the DM relays
signal_tx.send(NostrSignal::DmRelaysFound).await.ok();
if client
.subscribe_with_id_to(relays.clone(), sub_id, filter, None)
.await
.is_ok()
{
log::info!("Subscribing to messages in: {relays:?}");
}
}
}
}
Kind::ContactList => {
if let Ok(true) = is_from_current_user(&event).await {
let public_keys: Vec<PublicKey> = event.tags.public_keys().copied().collect();
let kinds = vec![Kind::Metadata, Kind::ContactList];
let lens = public_keys.len() * kinds.len();
let filter = Filter::new().limit(lens).authors(public_keys).kinds(kinds);
client
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(auto_close))
.await
.ok();
}
}
Kind::Metadata => {
signal_tx signal_tx
.send(NostrSignal::Eose(subscription_id.into_owned())) .send(NostrSignal::Metadata(event.into_owned()))
.await?; .await
.ok();
}
Kind::GiftWrap => {
event_tx.send(event.into_owned()).await.ok();
} }
_ => {} _ => {}
} }
@@ -449,28 +456,28 @@ async fn handle_nostr_notifications(
Ok(()) Ok(())
} }
async fn check_author(client: &Client, event: &Event) -> Result<bool, Error> { async fn is_from_current_user(event: &Event) -> Result<bool, Error> {
let client = nostr_client();
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
Ok(public_key == event.pubkey) Ok(public_key == event.pubkey)
} }
async fn sync_data_for_pubkeys(client: &Client, public_keys: BTreeSet<PublicKey>) { async fn sync_data_for_pubkeys(public_keys: BTreeSet<PublicKey>) {
let client = nostr_client();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let kinds = vec![Kind::Metadata, Kind::ContactList, Kind::RelayList]; let kinds = vec![Kind::Metadata, Kind::ContactList];
let filter = Filter::new() let filter = Filter::new()
.limit(public_keys.len() * kinds.len()) .limit(public_keys.len() * kinds.len())
.authors(public_keys) .authors(public_keys)
.kinds(kinds); .kinds(kinds);
if let Err(e) = client client
.subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts)) .subscribe_to(BOOTSTRAP_RELAYS, filter, Some(opts))
.await .await
{ .ok();
log::error!("Failed to sync metadata: {e}");
}
} }
/// Stores an unwrapped event in local database with reference to original /// Stores an unwrapped event in local database with reference to original
@@ -515,10 +522,9 @@ async fn get_unwrapped(root: EventId) -> Result<Event, Error> {
/// Unwraps a gift-wrapped event and processes its contents. /// Unwraps a gift-wrapped event and processes its contents.
async fn try_unwrap_event( async fn try_unwrap_event(
event: &Event,
signal_tx: &Sender<NostrSignal>, signal_tx: &Sender<NostrSignal>,
mta_tx: &Sender<PublicKey>, mta_tx: &Sender<PublicKey>,
event: &Event,
incoming: bool,
) -> bool { ) -> bool {
let client = nostr_client(); let client = nostr_client();
let mut is_cached = false; let mut is_cached = false;
@@ -552,16 +558,13 @@ async fn try_unwrap_event(
} }
}; };
// Get all pubkeys from the event
let all_pubkeys = event.all_pubkeys();
// Send all pubkeys to the metadata batch to sync data // Send all pubkeys to the metadata batch to sync data
for public_key in all_pubkeys { for public_key in event.all_pubkeys() {
mta_tx.send(public_key).await.ok(); mta_tx.send(public_key).await.ok();
} }
// Send a notify to GPUI if this is a new message // Send a notify to GPUI if this is a new message
if incoming { if starting_time() <= &event.created_at {
signal_tx.send(NostrSignal::GiftWrap(event)).await.ok(); signal_tx.send(NostrSignal::GiftWrap(event)).await.ok();
} }
+45 -47
View File
@@ -1,5 +1,4 @@
use std::cell::RefCell; use std::collections::{BTreeSet, HashMap};
use std::collections::HashMap;
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
@@ -10,10 +9,10 @@ use global::nostr_client;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, img, list, px, red, rems, white, Action, AnyElement, App, AppContext, ClipboardItem, div, img, list, px, red, rems, white, Action, AnyElement, App, AppContext, ClipboardItem,
Context, Element, Empty, Entity, EventEmitter, Flatten, FocusHandle, Focusable, Context, Element, Entity, EventEmitter, Flatten, FocusHandle, Focusable, InteractiveElement,
InteractiveElement, IntoElement, ListAlignment, ListState, MouseButton, ObjectFit, IntoElement, ListAlignment, ListState, MouseButton, ObjectFit, ParentElement,
ParentElement, PathPromptOptions, Render, RetainAllImageCache, SharedString, PathPromptOptions, Render, RetainAllImageCache, SharedString, StatefulInteractiveElement,
StatefulInteractiveElement, Styled, StyledImage, Subscription, Window, Styled, StyledImage, Subscription, Window,
}; };
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use i18n::t; use i18n::t;
@@ -43,6 +42,9 @@ use ui::{
mod subject; mod subject;
const DUPLICATE_TIME_WINDOW: u64 = 10;
const MAX_RECENT_MESSAGES_TO_CHECK: usize = 5;
#[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[derive(Action, Clone, PartialEq, Eq, Deserialize)]
#[action(namespace = chat, no_json)] #[action(namespace = chat, no_json)]
pub struct ChangeSubject(pub String); pub struct ChangeSubject(pub String);
@@ -57,7 +59,7 @@ pub struct Chat {
focus_handle: FocusHandle, focus_handle: FocusHandle,
// Chat Room // Chat Room
room: Entity<Room>, room: Entity<Room>,
messages: Entity<Vec<Rc<RefCell<Message>>>>, messages: Entity<BTreeSet<Message>>,
text_data: HashMap<EventId, RichText>, text_data: HashMap<EventId, RichText>,
list_state: ListState, list_state: ListState,
// New Message // New Message
@@ -76,7 +78,7 @@ impl Chat {
pub fn new(room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Self> { pub fn new(room: Entity<Room>, window: &mut Window, cx: &mut App) -> Entity<Self> {
let attaches = cx.new(|_| None); let attaches = cx.new(|_| None);
let replies_to = cx.new(|_| None); let replies_to = cx.new(|_| None);
let messages = cx.new(|_| vec![]); let messages = cx.new(|_| BTreeSet::new());
let input = cx.new(|cx| { let input = cx.new(|cx| {
InputState::new(window, cx) InputState::new(window, cx)
@@ -120,10 +122,10 @@ impl Chat {
} }
let old_len = this.messages.read(cx).len(); let old_len = this.messages.read(cx).len();
let message = event.clone().into_rc(); let message = event.to_owned();
cx.update_entity(&this.messages, |this, cx| { cx.update_entity(&this.messages, |this, cx| {
this.extend(vec![message]); this.insert(message);
cx.notify(); cx.notify();
}); });
@@ -132,21 +134,13 @@ impl Chat {
RoomSignal::Refresh => { RoomSignal::Refresh => {
this.load_messages(window, cx); this.load_messages(window, cx);
} }
} };
}, },
)); ));
// Initialize list state // Initialize list state
// [item_count] always equal to 1 at the beginning // [item_count] always equal to 1 at the beginning
let list_state = ListState::new(1, ListAlignment::Bottom, px(1024.), { let list_state = ListState::new(1, ListAlignment::Bottom, px(1024.));
let this = cx.entity().downgrade();
move |ix, window, cx| {
this.update(cx, |this, cx| {
this.render_message(ix, window, cx).into_any_element()
})
.unwrap_or(Empty.into_any())
}
});
Self { Self {
id: room.read(cx).id.to_string().into(), id: room.read(cx).id.to_string().into(),
@@ -179,7 +173,7 @@ impl Chat {
// Extend the messages list with the new events // Extend the messages list with the new events
this.messages.update(cx, |this, cx| { this.messages.update(cx, |this, cx| {
this.extend(messages.into_iter().map(|e| e.into_rc())); this.extend(messages);
cx.notify(); cx.notify();
}); });
@@ -237,14 +231,18 @@ impl Chat {
return false; return false;
} }
let min_timestamp = new_msg.created_at.as_u64().saturating_sub(10); let messages = self.messages.read(cx);
let min_timestamp = new_msg
.created_at
.as_u64()
.saturating_sub(DUPLICATE_TIME_WINDOW);
self.messages messages
.read(cx)
.iter() .iter()
.filter(|m| m.borrow().author == identity) .rev()
.take(MAX_RECENT_MESSAGES_TO_CHECK)
.filter(|m| m.author == identity)
.any(|existing| { .any(|existing| {
let existing = existing.borrow();
// Check if messages are within the time window // Check if messages are within the time window
(existing.created_at.as_u64() >= min_timestamp) && (existing.created_at.as_u64() >= min_timestamp) &&
// Compare content and author // Compare content and author
@@ -316,10 +314,9 @@ impl Chat {
}); });
this.messages.update(cx, |this, cx| { this.messages.update(cx, |this, cx| {
if let Some(msg) = if let Some(mut msg) = this.iter().find(|msg| msg.id == id).cloned()
this.iter().find(|msg| msg.borrow().id == id).cloned()
{ {
msg.borrow_mut().errors = Some(reports); msg.errors = Some(reports);
cx.notify(); cx.notify();
} }
}); });
@@ -334,10 +331,9 @@ impl Chat {
fn insert_message(&self, message: Message, cx: &mut Context<Self>) { fn insert_message(&self, message: Message, cx: &mut Context<Self>) {
let old_len = self.messages.read(cx).len(); let old_len = self.messages.read(cx).len();
let message = message.into_rc();
cx.update_entity(&self.messages, |this, cx| { cx.update_entity(&self.messages, |this, cx| {
this.extend(vec![message]); this.insert(message);
cx.notify(); cx.notify();
}); });
@@ -345,12 +341,7 @@ impl Chat {
} }
fn scroll_to(&self, id: EventId, cx: &Context<Self>) { fn scroll_to(&self, id: EventId, cx: &Context<Self>) {
if let Some(ix) = self if let Some(ix) = self.messages.read(cx).iter().position(|m| m.id == id) {
.messages
.read(cx)
.iter()
.position(|m| m.borrow().id == id)
{
self.list_state.scroll_to_reveal_item(ix); self.list_state.scroll_to_reveal_item(ix);
} }
} }
@@ -359,8 +350,9 @@ impl Chat {
let Some(item) = self let Some(item) = self
.messages .messages
.read(cx) .read(cx)
.get(ix) .iter()
.map(|m| ClipboardItem::new_string(m.borrow().content.to_string())) .nth(ix)
.map(|m| ClipboardItem::new_string(m.content.to_string()))
else { else {
return; return;
}; };
@@ -369,7 +361,7 @@ impl Chat {
} }
fn reply_to(&mut self, ix: usize, cx: &mut Context<Self>) { fn reply_to(&mut self, ix: usize, cx: &mut Context<Self>) {
let Some(message) = self.messages.read(cx).get(ix).map(|m| m.borrow().clone()) else { let Some(message) = self.messages.read(cx).iter().nth(ix).map(|m| m.to_owned()) else {
return; return;
}; };
@@ -583,7 +575,7 @@ impl Chat {
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> impl IntoElement { ) -> impl IntoElement {
let Some(message) = self.messages.read(cx).get(ix).map(|m| m.borrow()) else { let Some(message) = self.messages.read(cx).iter().nth(ix) else {
return div().id(ix); return div().id(ix);
}; };
@@ -642,10 +634,7 @@ impl Chat {
let messages = self.messages.read(cx); let messages = self.messages.read(cx);
for (ix, id) in replies.iter().cloned().enumerate() { for (ix, id) in replies.iter().cloned().enumerate() {
let Some(message) = messages let Some(message) = messages.iter().find(|m| m.id == id)
.iter()
.map(|m| m.borrow())
.find(|m| m.id == id)
else { else {
continue; continue;
}; };
@@ -723,7 +712,7 @@ impl Chat {
window.open_modal(cx, move |this, _window, cx| { window.open_modal(cx, move |this, _window, cx| {
this.title(SharedString::new(t!("chat.logs_title"))).child( this.title(SharedString::new(t!("chat.logs_title"))).child(
div() div()
.w_full() .pb_4()
.flex() .flex()
.flex_col() .flex_col()
.gap_2() .gap_2()
@@ -875,10 +864,19 @@ impl Focusable for Chat {
impl Render for Chat { impl Render for Chat {
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 {
let entity = cx.entity();
v_flex() v_flex()
.image_cache(self.image_cache.clone()) .image_cache(self.image_cache.clone())
.size_full() .size_full()
.child(list(self.list_state.clone()).flex_1()) .child(
list(self.list_state.clone(), move |ix, window, cx| {
entity.update(cx, |this, cx| {
this.render_message(ix, window, cx).into_any_element()
})
})
.flex_1(),
)
.child( .child(
div() div()
.flex_shrink_0() .flex_shrink_0()
+6 -27
View File
@@ -1,7 +1,7 @@
use std::time::Duration; use std::time::Duration;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use global::constants::{NEW_MESSAGE_ID, NIP17_RELAYS}; use global::constants::NIP17_RELAYS;
use global::nostr_client; use global::nostr_client;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
@@ -10,8 +10,6 @@ use gpui::{
TextAlign, UniformList, Window, TextAlign, UniformList, Window,
}; };
use i18n::{shared_t, t}; use i18n::{shared_t, t};
use identity::Identity;
use itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme; use theme::ActiveTheme;
@@ -189,15 +187,12 @@ impl MessagingRelays {
let task: Task<Result<(), Error>> = cx.background_spawn(async move { let task: Task<Result<(), Error>> = cx.background_spawn(async move {
let client = nostr_client(); let client = nostr_client();
let signer = client.signer().await?; let tags: Vec<Tag> = relays
let public_key = signer.get_public_key().await?; .iter()
.map(|relay| Tag::relay(relay.clone()))
.collect();
let builder = EventBuilder::new(Kind::InboxRelays, "").tags( let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags);
relays
.iter()
.map(|relay| Tag::relay(relay.clone()))
.collect_vec(),
);
// Set messaging relays // Set messaging relays
client.send_event_builder(builder).await?; client.send_event_builder(builder).await?;
@@ -208,18 +203,6 @@ impl MessagingRelays {
_ = client.connect_relay(&relay).await; _ = client.connect_relay(&relay).await;
} }
let id = SubscriptionId::new(NEW_MESSAGE_ID);
let new_messages = Filter::new()
.kind(Kind::GiftWrap)
.pubkey(public_key)
.limit(0);
// Close old subscriptions
client.unsubscribe(&id).await;
// Subscribe to new messages
client.subscribe_with_id(id, new_messages, None).await?;
Ok(()) Ok(())
}); });
@@ -227,10 +210,6 @@ impl MessagingRelays {
match task.await { match task.await {
Ok(_) => { Ok(_) => {
cx.update(|window, cx| { cx.update(|window, cx| {
Identity::global(cx).update(cx, |this, cx| {
this.verify_dm_relays(window, cx);
});
// Close the current modal
window.close_modal(cx); window.close_modal(cx);
}) })
.ok(); .ok();
+83 -33
View File
@@ -15,6 +15,7 @@ use ui::actions::OpenProfile;
use ui::avatar::Avatar; use ui::avatar::Avatar;
use ui::context_menu::ContextMenuExt; use ui::context_menu::ContextMenuExt;
use ui::modal::ModalButtonProps; use ui::modal::ModalButtonProps;
use ui::skeleton::Skeleton;
use ui::{h_flex, ContextModal, StyledExt}; use ui::{h_flex, ContextModal, StyledExt};
use crate::views::screening; use crate::views::screening;
@@ -23,60 +24,109 @@ use crate::views::screening;
pub struct RoomListItem { pub struct RoomListItem {
ix: usize, ix: usize,
base: Div, base: Div,
room_id: u64, room_id: Option<u64>,
public_key: PublicKey, public_key: Option<PublicKey>,
name: SharedString, name: Option<SharedString>,
avatar: SharedString, avatar: Option<SharedString>,
created_at: SharedString, created_at: Option<SharedString>,
kind: RoomKind, kind: Option<RoomKind>,
#[allow(clippy::type_complexity)] #[allow(clippy::type_complexity)]
handler: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>, handler: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
} }
impl RoomListItem { impl RoomListItem {
pub fn new( pub fn new(ix: usize) -> Self {
ix: usize,
room_id: u64,
public_key: PublicKey,
name: SharedString,
avatar: SharedString,
created_at: SharedString,
kind: RoomKind,
) -> Self {
Self { Self {
ix, ix,
public_key, base: h_flex().h_9().w_full().px_1p5().gap_2(),
room_id, room_id: None,
name, public_key: None,
avatar, name: None,
created_at, avatar: None,
kind, created_at: None,
base: h_flex().h_9().w_full().px_1p5(), kind: None,
handler: Rc::new(|_, _, _| {}), handler: None,
} }
} }
pub fn room_id(mut self, room_id: u64) -> Self {
self.room_id = Some(room_id);
self
}
pub fn public_key(mut self, public_key: PublicKey) -> Self {
self.public_key = Some(public_key);
self
}
pub fn name(mut self, name: SharedString) -> Self {
self.name = Some(name);
self
}
pub fn avatar(mut self, avatar: SharedString) -> Self {
self.avatar = Some(avatar);
self
}
pub fn created_at(mut self, created_at: SharedString) -> Self {
self.created_at = Some(created_at);
self
}
pub fn kind(mut self, kind: RoomKind) -> Self {
self.kind = Some(kind);
self
}
pub fn on_click( pub fn on_click(
mut self, mut self,
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self { ) -> Self {
self.handler = Rc::new(handler); self.handler = Some(Rc::new(handler));
self self
} }
} }
impl RenderOnce for RoomListItem { impl RenderOnce for RoomListItem {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let public_key = self.public_key;
let room_id = self.room_id;
let kind = self.kind;
let handler = self.handler.clone();
let hide_avatar = AppSettings::get_hide_user_avatars(cx); let hide_avatar = AppSettings::get_hide_user_avatars(cx);
let require_screening = AppSettings::get_screening(cx); let require_screening = AppSettings::get_screening(cx);
let (
Some(public_key),
Some(room_id),
Some(name),
Some(avatar),
Some(created_at),
Some(kind),
Some(handler),
) = (
self.public_key,
self.room_id,
self.name,
self.avatar,
self.created_at,
self.kind,
self.handler,
)
else {
return self
.base
.id(self.ix)
.child(Skeleton::new().flex_shrink_0().size_6().rounded_full())
.child(
div()
.flex_1()
.flex()
.justify_between()
.child(Skeleton::new().w_32().h_2p5().rounded_sm())
.child(Skeleton::new().w_6().h_2p5().rounded_sm()),
);
};
self.base self.base
.id(self.ix) .id(self.ix)
.gap_2()
.text_sm() .text_sm()
.rounded(cx.theme().radius) .rounded(cx.theme().radius)
.when(!hide_avatar, |this| { .when(!hide_avatar, |this| {
@@ -86,7 +136,7 @@ impl RenderOnce for RoomListItem {
.size_6() .size_6()
.rounded_full() .rounded_full()
.overflow_hidden() .overflow_hidden()
.child(Avatar::new(self.avatar).size(rems(1.5))), .child(Avatar::new(avatar).size(rems(1.5))),
) )
}) })
.child( .child(
@@ -102,14 +152,14 @@ impl RenderOnce for RoomListItem {
.text_ellipsis() .text_ellipsis()
.truncate() .truncate()
.font_medium() .font_medium()
.child(self.name), .child(name),
) )
.child( .child(
div() div()
.flex_shrink_0() .flex_shrink_0()
.text_xs() .text_xs()
.text_color(cx.theme().text_placeholder) .text_color(cx.theme().text_placeholder)
.child(self.created_at), .child(created_at),
), ),
) )
.context_menu(move |this, _window, _cx| { .context_menu(move |this, _window, _cx| {
+64 -194
View File
@@ -9,9 +9,9 @@ use global::constants::{BOOTSTRAP_RELAYS, SEARCH_RELAYS};
use global::nostr_client; use global::nostr_client;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, relative, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter, div, uniform_list, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
FocusHandle, Focusable, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString, Focusable, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString, Styled,
Styled, Subscription, Task, Window, Subscription, Task, Window,
}; };
use gpui_tokio::Tokio; use gpui_tokio::Tokio;
use i18n::t; use i18n::t;
@@ -26,16 +26,15 @@ use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::button::{Button, ButtonRounded, ButtonVariants}; use ui::button::{Button, ButtonRounded, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent}; use ui::dock_area::panel::{Panel, PanelEvent};
use ui::indicator::Indicator;
use ui::input::{InputEvent, InputState, TextInput}; use ui::input::{InputEvent, InputState, TextInput};
use ui::popup_menu::PopupMenu; use ui::popup_menu::PopupMenu;
use ui::skeleton::Skeleton;
use ui::{v_flex, ContextModal, IconName, Selectable, Sizable, StyledExt}; use ui::{v_flex, ContextModal, IconName, Selectable, Sizable, StyledExt};
mod list_item; mod list_item;
const FIND_DELAY: u64 = 600; const FIND_DELAY: u64 = 600;
const FIND_LIMIT: usize = 10; const FIND_LIMIT: usize = 10;
const TOTAL_SKELETONS: usize = 3;
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)
@@ -547,60 +546,6 @@ impl Sidebar {
}); });
} }
fn open_loading_modal(&self, window: &mut Window, cx: &mut Context<Self>) {
let title = SharedString::new(t!("sidebar.loading_modal_title"));
let text_1 = SharedString::new(t!("sidebar.loading_modal_body_1"));
let text_2 = SharedString::new(t!("sidebar.loading_modal_body_2"));
let desc = SharedString::new(t!("sidebar.loading_modal_description"));
window.open_modal(cx, move |this, _window, cx| {
this.show_close(true)
.keyboard(true)
.title(title.clone())
.child(
v_flex()
.pb_4()
.gap_2()
.child(
div()
.flex()
.flex_col()
.gap_2()
.text_sm()
.child(text_1.clone())
.child(text_2.clone()),
)
.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(desc.clone()),
),
)
});
}
fn skeletons(&self, total: i32) -> impl IntoIterator<Item = impl IntoElement> {
(0..total).map(|_| {
div()
.h_9()
.w_full()
.px_1p5()
.flex()
.items_center()
.gap_2()
.child(Skeleton::new().flex_shrink_0().size_6().rounded_full())
.child(
div()
.flex_1()
.flex()
.justify_between()
.child(Skeleton::new().w_32().h_2p5().rounded_sm())
.child(Skeleton::new().w_6().h_2p5().rounded_sm()),
)
})
}
fn list_items( fn list_items(
&self, &self,
rooms: &[Entity<Room>], rooms: &[Entity<Room>],
@@ -621,17 +566,17 @@ impl Sidebar {
}); });
items.push( items.push(
RoomListItem::new( RoomListItem::new(ix)
ix, .room_id(room_id)
room_id, .name(this.display_name(cx))
this.members[0], .avatar(this.display_image(proxy, cx))
this.display_name(cx), .created_at(this.ago())
this.display_image(proxy, cx), .public_key(this.members[0])
this.ago(), .kind(this.kind)
this.kind, .on_click(handler),
)
.on_click(handler),
) )
} else {
items.push(RoomListItem::new(ix));
} }
} }
@@ -668,6 +613,7 @@ 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 {
let registry = Registry::read_global(cx); let registry = Registry::read_global(cx);
let loading = registry.loading;
// Get rooms from either search results or the chat registry // Get rooms from either search results or the chat registry
let rooms = if let Some(results) = self.local_result.read(cx).as_ref() { let rooms = if let Some(results) = self.local_result.read(cx).as_ref() {
@@ -683,6 +629,15 @@ impl Render for Sidebar {
} }
}; };
// Get total rooms count
let mut total_rooms = rooms.len();
// If loading in progress
// Add 3 skeletons to the room list
if loading {
total_rooms += TOTAL_SKELETONS;
}
v_flex() v_flex()
.image_cache(self.image_cache.clone()) .image_cache(self.image_cache.clone())
.size_full() .size_full()
@@ -722,86 +677,55 @@ impl Render for Sidebar {
.overflow_y_hidden() .overflow_y_hidden()
.child( .child(
div() div()
.flex_none()
.px_1() .px_1()
.w_full() .h_flex()
.h_9() .gap_2()
.flex() .flex_none()
.items_center()
.justify_between()
.child( .child(
div() Button::new("all")
.flex() .label(t!("sidebar.all_button"))
.items_center() .tooltip(t!("sidebar.all_conversations_tooltip"))
.gap_2() .when_some(self.indicator.read(cx).as_ref(), |this, kind| {
.child( this.when(kind == &RoomKind::Ongoing, |this| {
Button::new("all") this.child(
.label(t!("sidebar.all_button")) div().size_1().rounded_full().bg(cx.theme().cursor),
.tooltip(t!("sidebar.all_conversations_tooltip"))
.when_some(
self.indicator.read(cx).as_ref(),
|this, kind| {
this.when(kind == &RoomKind::Ongoing, |this| {
this.child(
div()
.size_1()
.rounded_full()
.bg(cx.theme().cursor),
)
})
},
) )
.small() })
.bold() })
.secondary() .small()
.rounded(ButtonRounded::Full) .bold()
.selected(self.filter(&RoomKind::Ongoing, cx)) .secondary()
.on_click(cx.listener(|this, _, _, cx| { .rounded(ButtonRounded::Full)
this.set_filter(RoomKind::Ongoing, cx); .selected(self.filter(&RoomKind::Ongoing, cx))
})), .on_click(cx.listener(|this, _, _, cx| {
) this.set_filter(RoomKind::Ongoing, cx);
.child( })),
Button::new("requests") )
.label(t!("sidebar.requests_button")) .child(
.tooltip(t!("sidebar.requests_tooltip")) Button::new("requests")
.when_some( .label(t!("sidebar.requests_button"))
self.indicator.read(cx).as_ref(), .tooltip(t!("sidebar.requests_tooltip"))
|this, kind| { .when_some(self.indicator.read(cx).as_ref(), |this, kind| {
this.when(kind != &RoomKind::Ongoing, |this| { this.when(kind != &RoomKind::Ongoing, |this| {
this.child( this.child(
div() div().size_1().rounded_full().bg(cx.theme().cursor),
.size_1()
.rounded_full()
.bg(cx.theme().cursor),
)
})
},
) )
.small() })
.bold() })
.secondary() .small()
.rounded(ButtonRounded::Full) .bold()
.selected(!self.filter(&RoomKind::Ongoing, cx)) .secondary()
.on_click(cx.listener(|this, _, _, cx| { .rounded(ButtonRounded::Full)
this.set_filter(RoomKind::default(), cx); .selected(!self.filter(&RoomKind::Ongoing, cx))
})), .on_click(cx.listener(|this, _, _, cx| {
), this.set_filter(RoomKind::default(), cx);
})),
), ),
) )
.when(registry.loading, |this| {
this.child(
div()
.flex_1()
.flex()
.flex_col()
.gap_1()
.children(self.skeletons(1)),
)
})
.child( .child(
uniform_list( uniform_list(
"rooms", "rooms",
rooms.len(), total_rooms,
cx.processor(move |this, range, _window, cx| { cx.processor(move |this, range, _window, cx| {
this.list_items(&rooms, range, cx) this.list_items(&rooms, range, cx)
}), }),
@@ -809,59 +733,5 @@ impl Render for Sidebar {
.h_full(), .h_full(),
), ),
) )
.when(registry.loading, |this| {
let title = SharedString::new(t!("sidebar.retrieving_messages"));
let desc = SharedString::new(t!("sidebar.retrieving_messages_description"));
this.child(
div().absolute().bottom_3().px_3().w_full().child(
div()
.p_1()
.w_full()
.rounded_full()
.flex()
.items_center()
.justify_between()
.bg(cx.theme().panel_background)
.shadow_sm()
// Loading
.child(div().flex_shrink_0().pl_1().child(Indicator::new().small()))
// Title
.child(
v_flex()
.flex_1()
.items_center()
.justify_center()
.text_center()
.child(
div()
.text_sm()
.font_semibold()
.line_height(relative(1.2))
.child(title.clone()),
)
.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(desc.clone()),
),
)
// Info button
.child(
Button::new("help")
.icon(IconName::Info)
.tooltip(t!("sidebar.why_seeing_this_tooltip"))
.small()
.ghost()
.rounded(ButtonRounded::Full)
.flex_shrink_0()
.on_click(cx.listener(move |this, _, window, cx| {
this.open_loading_modal(window, cx)
})),
),
),
)
})
} }
} }
+9 -12
View File
@@ -8,10 +8,9 @@ pub const ACCOUNT_D: &str = "coop:account";
pub const SETTINGS_D: &str = "coop:settings"; pub const SETTINGS_D: &str = "coop:settings";
/// Bootstrap Relays. /// Bootstrap Relays.
pub const BOOTSTRAP_RELAYS: [&str; 5] = [ pub const BOOTSTRAP_RELAYS: [&str; 4] = [
"wss://relay.damus.io", "wss://relay.damus.io",
"wss://relay.primal.net", "wss://relay.primal.net",
"wss://nostr.Wine",
"wss://user.kindpag.es", "wss://user.kindpag.es",
"wss://purplepag.es", "wss://purplepag.es",
]; ];
@@ -36,22 +35,20 @@ pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
/// Default timeout (in seconds) for Nostr Connect /// Default timeout (in seconds) for Nostr Connect
pub const NOSTR_CONNECT_TIMEOUT: u64 = 200; pub const NOSTR_CONNECT_TIMEOUT: u64 = 200;
/// Unique ID for new message subscription.
pub const NEW_MESSAGE_ID: &str = "listen_new_giftwraps";
/// Unique ID for all messages subscription.
pub const ALL_MESSAGES_ID: &str = "listen_all_giftwraps";
/// Unique ID for all newest messages subscription.
pub const ALL_NEWEST_MESSAGES_ID: &str = "listen_all_newest_giftwraps";
/// Total metadata requests will be grouped. /// Total metadata requests will be grouped.
pub const METADATA_BATCH_LIMIT: usize = 100; pub const METADATA_BATCH_LIMIT: usize = 100;
/// Maximum timeout for grouping metadata requests.
pub const METADATA_BATCH_TIMEOUT: u64 = 400; /// Maximum timeout for grouping metadata requests. (milliseconds)
pub const METADATA_BATCH_TIMEOUT: u64 = 300;
/// Maximum timeout for waiting for finish (seconds)
pub const WAIT_FOR_FINISH: u64 = 60;
/// Default width for all modals. /// Default width for all modals.
pub const DEFAULT_MODAL_WIDTH: f32 = 420.; pub const DEFAULT_MODAL_WIDTH: f32 = 420.;
/// Default width of the sidebar. /// Default width of the sidebar.
pub const DEFAULT_SIDEBAR_WIDTH: f32 = 280.; pub const DEFAULT_SIDEBAR_WIDTH: f32 = 240.;
/// Image Resize Service /// Image Resize Service
pub const IMAGE_RESIZE_SERVICE: &str = "https://wsrv.nl"; pub const IMAGE_RESIZE_SERVICE: &str = "https://wsrv.nl";
+17 -20
View File
@@ -1,9 +1,11 @@
use std::collections::BTreeSet;
use std::sync::OnceLock; use std::sync::OnceLock;
use std::time::Duration; use std::time::Duration;
use nostr_connect::prelude::*; use nostr_connect::prelude::*;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use paths::nostr_file; use paths::nostr_file;
use smol::lock::RwLock;
use crate::paths::support_dir; use crate::paths::support_dir;
@@ -25,14 +27,16 @@ pub enum NostrSignal {
/// Partially finished processing all gift wrap events /// Partially finished processing all gift wrap events
PartialFinish, PartialFinish,
/// Receives EOSE response from relay pool /// DM relays have been found
Eose(SubscriptionId), DmRelaysFound,
/// Notice from Relay Pool /// Notice from Relay Pool
Notice(String), Notice(String),
} }
static NOSTR_CLIENT: OnceLock<Client> = OnceLock::new(); static NOSTR_CLIENT: OnceLock<Client> = OnceLock::new();
static PROCESSED_EVENTS: OnceLock<RwLock<BTreeSet<EventId>>> = OnceLock::new();
static CURRENT_TIMESTAMP: OnceLock<Timestamp> = OnceLock::new();
static FIRST_RUN: OnceLock<bool> = OnceLock::new(); static FIRST_RUN: OnceLock<bool> = OnceLock::new();
pub fn nostr_client() -> &'static Client { pub fn nostr_client() -> &'static Client {
@@ -47,20 +51,26 @@ pub fn nostr_client() -> &'static Client {
let lmdb = NostrLMDB::open(nostr_file()).expect("Database is NOT initialized"); let lmdb = NostrLMDB::open(nostr_file()).expect("Database is NOT initialized");
let opts = ClientOptions::new() let opts = ClientOptions::new()
// Coop isn't social client,
// but it needs this option because it needs user's NIP65 Relays to fetch NIP17 Relays.
.gossip(true) .gossip(true)
// TODO: Coop should handle authentication by itself
.automatic_authentication(true) .automatic_authentication(true)
// Sleep after idle for 5 seconds .verify_subscriptions(false)
// Sleep after idle for 30 seconds
.sleep_when_idle(SleepWhenIdle::Enabled { .sleep_when_idle(SleepWhenIdle::Enabled {
timeout: Duration::from_secs(10), timeout: Duration::from_secs(30),
}); });
ClientBuilder::default().database(lmdb).opts(opts).build() ClientBuilder::default().database(lmdb).opts(opts).build()
}) })
} }
pub fn processed_events() -> &'static RwLock<BTreeSet<EventId>> {
PROCESSED_EVENTS.get_or_init(|| RwLock::new(BTreeSet::new()))
}
pub fn starting_time() -> &'static Timestamp {
CURRENT_TIMESTAMP.get_or_init(Timestamp::now)
}
pub fn first_run() -> &'static bool { pub fn first_run() -> &'static bool {
FIRST_RUN.get_or_init(|| { FIRST_RUN.get_or_init(|| {
let flag = support_dir().join(format!(".{}-first_run", env!("CARGO_PKG_VERSION"))); let flag = support_dir().join(format!(".{}-first_run", env!("CARGO_PKG_VERSION")));
@@ -75,16 +85,3 @@ pub fn first_run() -> &'static bool {
} }
}) })
} }
pub async fn set_all_gift_wraps_fetched() {
let flag = support_dir().join(".fetched");
if !flag.exists() && smol::fs::write(&flag, "").await.is_err() {
log::error!("Failed to create full run flag");
}
}
pub fn is_gift_wraps_fetch_complete() -> bool {
let flag = support_dir().join(".fetched");
flag.exists()
}
+58 -139
View File
@@ -3,11 +3,8 @@ use std::time::Duration;
use anyhow::{anyhow, Error}; use anyhow::{anyhow, Error};
use client_keys::ClientKeys; use client_keys::ClientKeys;
use common::handle_auth::CoopAuthUrlHandler; use common::handle_auth::CoopAuthUrlHandler;
use global::constants::{ use global::constants::{ACCOUNT_D, NIP17_RELAYS, NIP65_RELAYS, NOSTR_CONNECT_TIMEOUT};
ACCOUNT_D, ALL_MESSAGES_ID, ALL_NEWEST_MESSAGES_ID, NEW_MESSAGE_ID, NIP17_RELAYS, NIP65_RELAYS, use global::nostr_client;
NOSTR_CONNECT_TIMEOUT,
};
use global::{is_gift_wraps_fetch_complete, nostr_client};
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, red, App, AppContext, Context, Entity, Global, ParentElement, SharedString, Styled, div, red, App, AppContext, Context, Entity, Global, ParentElement, SharedString, Styled,
@@ -32,7 +29,7 @@ impl Global for GlobalIdentity {}
pub struct Identity { pub struct Identity {
public_key: Option<PublicKey>, public_key: Option<PublicKey>,
logging_in: bool, logging_in: bool,
relay_ready: Option<bool>, has_dm_relays: Option<bool>,
need_backup: Option<Keys>, need_backup: Option<Keys>,
need_onboarding: bool, need_onboarding: bool,
#[allow(dead_code)] #[allow(dead_code)]
@@ -76,8 +73,8 @@ impl Identity {
Self { Self {
public_key: None, public_key: None,
relay_ready: None,
need_backup: None, need_backup: None,
has_dm_relays: None,
need_onboarding: false, need_onboarding: false,
logging_in: false, logging_in: false,
subscriptions, subscriptions,
@@ -130,8 +127,10 @@ impl Identity {
.kind(Kind::ApplicationSpecificData) .kind(Kind::ApplicationSpecificData)
.identifier(ACCOUNT_D); .identifier(ACCOUNT_D);
// Unset signer // Reset the nostr client
client.unset_signer().await; client.unset_signer().await;
client.unsubscribe_all().await;
// Delete account // Delete account
client.database().delete(filter).await?; client.database().delete(filter).await?;
@@ -259,7 +258,7 @@ impl Identity {
this.set_public_key(None, window, cx); this.set_public_key(None, window, cx);
}) })
.ok(); .ok();
// Close modal // true to close the modal
true true
}) })
.on_ok(move |_, window, cx| { .on_ok(move |_, window, cx| {
@@ -273,7 +272,7 @@ impl Identity {
this.verify_keys(enc, password, weak_error, window, cx); this.verify_keys(enc, password, weak_error, window, cx);
}) })
.ok(); .ok();
// false to keep the modal open
false false
}) })
.child( .child(
@@ -323,32 +322,40 @@ impl Identity {
} }
// Decrypt the password in the background to prevent blocking the main thread // Decrypt the password in the background to prevent blocking the main thread
let task: Task<Option<SecretKey>> = let task: Task<Result<SecretKey, Error>> = cx.background_spawn(async move {
cx.background_spawn(async move { enc.decrypt(&password).ok() }); let secret = enc.decrypt(&password)?;
Ok(secret)
});
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| {
if let Some(secret) = task.await { match task.await {
cx.update(|window, cx| { Ok(secret) => {
window.close_modal(cx); cx.update(|window, cx| {
// Update user's signer with decrypted secret key this.update(cx, |this, cx| {
this.update(cx, |this, cx| { // Update user's signer with decrypted secret key
this.set_signer(Keys::new(secret), window, cx); this.set_signer(Keys::new(secret), window, cx);
// Close the current modal
window.close_modal(cx);
})
.ok();
}) })
.ok(); .ok();
}) }
.ok(); Err(e) => {
} else { error
_ = error.update(cx, |this, cx| { .update(cx, |this, cx| {
*this = Some("Invalid password".into()); *this = Some(e.to_string().into());
cx.notify(); cx.notify();
}); })
.ok();
}
} }
}) })
.detach(); .detach();
} }
/// Sets a new signer for the client and updates user identity /// Sets a new signer for the client and updates user identity
pub fn set_signer<S>(&self, signer: S, window: &mut Window, cx: &mut Context<Self>) pub fn set_signer<S>(&mut self, signer: S, window: &mut Window, cx: &mut Context<Self>)
where where
S: NostrSigner + 'static, S: NostrSigner + 'static,
{ {
@@ -360,7 +367,7 @@ impl Identity {
client.set_signer(signer).await; client.set_signer(signer).await;
// Subscribe for user metadata // Subscribe for user metadata
Self::subscribe(client, public_key).await?; get_nip65_relays(public_key).await?;
Ok(public_key) Ok(public_key)
}); });
@@ -425,11 +432,12 @@ impl Identity {
// Set user's NIP65 relays // Set user's NIP65 relays
client.send_event_builder(relay_list).await?; client.send_event_builder(relay_list).await?;
// Set user's NIP17 relays // Set user's NIP17 relays
client.send_event_builder(dm_relay).await?; client.send_event_builder(dm_relay).await?;
// Subscribe for user metadata // Get user's NIP65 relays
Self::subscribe(client, public_key).await?; get_nip65_relays(public_key).await?;
Ok(public_key) Ok(public_key)
}); });
@@ -550,60 +558,15 @@ impl Identity {
.detach(); .detach();
} }
pub fn verify_dm_relays(&self, window: &mut Window, cx: &mut Context<Self>) {
let Some(public_key) = self.public_key() else {
return;
};
let task: Task<bool> = cx.background_spawn(async move {
let client = nostr_client();
let filter = Filter::new()
.kind(Kind::InboxRelays)
.author(public_key)
.limit(1);
let Ok(events) = client.database().query(filter).await else {
return false;
};
let Some(event) = events.first() else {
return false;
};
let relays: Vec<RelayUrl> = event
.tags
.filter(TagKind::Relay)
.filter_map(|tag| RelayUrl::parse(tag.content()?).ok())
.collect();
!relays.is_empty()
});
cx.spawn_in(window, async move |this, cx| {
let result = task.await;
this.update(cx, |this, cx| {
this.relay_ready = Some(result);
cx.notify();
})
.ok();
})
.detach();
}
/// Sets the public key of the identity /// Sets the public key of the identity
pub(crate) fn set_public_key( pub(crate) fn set_public_key(
&mut self, &mut self,
public_key: Option<PublicKey>, public_key: Option<PublicKey>,
window: &mut Window, _window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
self.public_key = public_key; self.public_key = public_key;
cx.notify(); cx.notify();
// Run verify user's dm relays task
cx.defer_in(window, |this, window, cx| {
this.verify_dm_relays(window, cx);
});
} }
/// Returns the current identity's public key /// Returns the current identity's public key
@@ -616,8 +579,9 @@ impl Identity {
self.public_key.is_some() self.public_key.is_some()
} }
pub fn relay_ready(&self) -> Option<bool> { /// Returns true if the identity has DM Relays
self.relay_ready pub fn has_dm_relays(&self) -> Option<bool> {
self.has_dm_relays
} }
/// Returns true if the identity is currently logging in /// Returns true if the identity is currently logging in
@@ -625,74 +589,29 @@ impl Identity {
self.logging_in self.logging_in
} }
/// Sets the DM Relays status of the identity
pub fn set_has_dm_relays(&mut self, cx: &mut Context<Self>) {
self.has_dm_relays = Some(true);
cx.notify();
}
/// Sets the logging in status of the identity /// Sets the logging in status of the identity
pub(crate) fn set_logging_in(&mut self, status: bool, cx: &mut Context<Self>) { pub(crate) fn set_logging_in(&mut self, status: bool, cx: &mut Context<Self>) {
self.logging_in = status; self.logging_in = status;
cx.notify(); cx.notify();
} }
}
pub(crate) async fn subscribe(client: &Client, public_key: PublicKey) -> Result<(), Error> { async fn get_nip65_relays(public_key: PublicKey) -> Result<(), Error> {
let all_messages = SubscriptionId::new(ALL_MESSAGES_ID); let client = nostr_client();
let all_newest_messages = SubscriptionId::new(ALL_NEWEST_MESSAGES_ID); let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
let new_messages = SubscriptionId::new(NEW_MESSAGE_ID); let sub_id = SubscriptionId::new("nip65-relays");
// Subscription options let filter = Filter::new()
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); .kind(Kind::RelayList)
// Get the gift wraps fetch status .author(public_key)
let is_completed = is_gift_wraps_fetch_complete(); .limit(1);
client client.subscribe_with_id(sub_id, filter, Some(opts)).await?;
.subscribe(
Filter::new()
.author(public_key)
.kinds(vec![Kind::Metadata, Kind::ContactList, Kind::RelayList])
.since(Timestamp::now()),
None,
)
.await?;
client Ok(())
.subscribe(
Filter::new()
.kinds(vec![Kind::Metadata, Kind::ContactList, Kind::RelayList])
.author(public_key)
.limit(10),
Some(opts),
)
.await?;
client
.subscribe_with_id(
new_messages,
Filter::new()
.kind(Kind::GiftWrap)
.pubkey(public_key)
.limit(0),
None,
)
.await?;
if is_completed {
let week_ago: u64 = 7 * 24 * 60 * 60;
let since = Timestamp::from_secs(Timestamp::now().as_u64() - week_ago);
let filter = Filter::new()
.pubkey(public_key)
.kind(Kind::GiftWrap)
.since(since);
client
.subscribe_with_id(all_newest_messages, filter, Some(opts))
.await?;
} else {
let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
client
.subscribe_with_id(all_messages, filter, Some(opts))
.await?;
};
log::info!("Getting all user's metadata and messages...");
Ok(())
}
} }
+6
View File
@@ -101,6 +101,12 @@ impl Registry {
} }
} }
pub fn reset(&mut self, cx: &mut Context<Self>) {
self.rooms = vec![];
self.loading = true;
cx.notify();
}
pub(crate) fn set_persons_from_task( pub(crate) fn set_persons_from_task(
&mut self, &mut self,
task: Task<Result<Vec<Profile>, Error>>, task: Task<Result<Vec<Profile>, Error>>,
+28 -18
View File
@@ -1,6 +1,5 @@
use std::cell::RefCell; use std::hash::Hash;
use std::iter::IntoIterator; use std::iter::IntoIterator;
use std::rc::Rc;
use chrono::{Local, TimeZone}; use chrono::{Local, TimeZone};
use gpui::SharedString; use gpui::SharedString;
@@ -12,7 +11,7 @@ use crate::room::SendError;
/// ///
/// Contains information about the message content, author, creation time, /// Contains information about the message content, author, creation time,
/// mentions, replies, and any errors that occurred during sending. /// mentions, replies, and any errors that occurred during sending.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone)]
pub struct Message { pub struct Message {
/// Unique identifier of the message (EventId from nostr_sdk) /// Unique identifier of the message (EventId from nostr_sdk)
pub id: EventId, pub id: EventId,
@@ -30,6 +29,32 @@ pub struct Message {
pub errors: Option<Vec<SendError>>, pub errors: Option<Vec<SendError>>,
} }
impl Eq for Message {}
impl PartialEq for Message {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Ord for Message {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.created_at.cmp(&other.created_at)
}
}
impl PartialOrd for Message {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Hash for Message {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
/// Builder pattern implementation for constructing Message objects. /// Builder pattern implementation for constructing Message objects.
#[derive(Debug)] #[derive(Debug)]
pub struct MessageBuilder { pub struct MessageBuilder {
@@ -110,11 +135,6 @@ impl MessageBuilder {
self self
} }
/// Builds the message wrapped in an Rc<RefCell<Message>>
pub fn build_rc(self) -> Result<Rc<RefCell<Message>>, String> {
self.build().map(|m| Rc::new(RefCell::new(m)))
}
/// Builds the message /// Builds the message
pub fn build(self) -> Result<Message, String> { pub fn build(self) -> Result<Message, String> {
Ok(Message { Ok(Message {
@@ -135,16 +155,6 @@ impl Message {
MessageBuilder::new(id, author) MessageBuilder::new(id, author)
} }
/// Converts the message into an Rc<RefCell<Message>>
pub fn into_rc(self) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(self))
}
/// Builds a message from a builder and wraps it in Rc<RefCell>
pub fn build_rc(builder: MessageBuilder) -> Result<Rc<RefCell<Self>>, String> {
builder.build().map(|m| Rc::new(RefCell::new(m)))
}
/// Returns a human-readable string representing how long ago the message was created /// Returns a human-readable string representing how long ago the message was created
pub fn ago(&self) -> SharedString { pub fn ago(&self) -> SharedString {
let input_time = match Local.timestamp_opt(self.created_at.as_u64() as i64, 0) { let input_time = match Local.timestamp_opt(self.created_at.as_u64() as i64, 0) {
+2 -2
View File
@@ -118,14 +118,14 @@ impl Render for TitleBar {
.w_full() .w_full()
.when(cx.theme().platform_kind.is_mac(), |this| { .when(cx.theme().platform_kind.is_mac(), |this| {
this.on_click(|event, window, _| { this.on_click(|event, window, _| {
if event.up.click_count == 2 { if event.click_count() == 2 {
window.titlebar_double_click(); window.titlebar_double_click();
} }
}) })
}) })
.when(cx.theme().platform_kind.is_linux(), |this| { .when(cx.theme().platform_kind.is_linux(), |this| {
this.on_click(|event, window, _| { this.on_click(|event, window, _| {
if event.up.click_count == 2 { if event.click_count() == 2 {
window.zoom_window(); window.zoom_window();
} }
}) })
+1 -1
View File
@@ -10,7 +10,7 @@ pub trait InteractiveElementExt: InteractiveElement {
Self: Sized, Self: Sized,
{ {
self.interactivity().on_click(move |event, window, cx| { self.interactivity().on_click(move |event, window, cx| {
if event.up.click_count == 2 { if event.click_count() == 2 {
listener(event, window, cx); listener(event, window, cx);
} }
}); });
+6 -14
View File
@@ -335,17 +335,9 @@ sidebar:
en: "Incoming new conversations" en: "Incoming new conversations"
trusted_contacts_tooltip: trusted_contacts_tooltip:
en: "Only show rooms from trusted contacts" en: "Only show rooms from trusted contacts"
retrieving_messages:
en: "Retrieving messages" loading:
retrieving_messages_description: label:
en: "This may take some time" en: "Downloading messages"
why_seeing_this_tooltip: tooltip:
en: "Why you're seeing this" en: "This may take a while. Please be patient."
loading_modal_title:
en: "Retrieving Your Messages"
loading_modal_body_1:
en: "Coop is downloading all your messages from the messaging relays. Depending on your total number of messages, this process may take up to 15 minutes if you're using Nostr Connect."
loading_modal_body_2:
en: "Please be patient - you only need to do this full download once. Next time, Coop will only download new messages."
loading_modal_description:
en: "You still can use the app normally while messages are processing in the background"
+1 -1
View File
@@ -3,7 +3,7 @@
flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo
arch=$(arch) arch=$(arch)
fd_version=23.08 fd_version=24.08
flatpak install -y --user org.freedesktop.Platform/${arch}/${fd_version} flatpak install -y --user org.freedesktop.Platform/${arch}/${fd_version}
flatpak install -y --user org.freedesktop.Sdk/${arch}/${fd_version} flatpak install -y --user org.freedesktop.Sdk/${arch}/${fd_version}
flatpak install -y --user org.freedesktop.Sdk.Extension.rust-stable/${arch}/${fd_version} flatpak install -y --user org.freedesktop.Sdk.Extension.rust-stable/${arch}/${fd_version}