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