7 Commits

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

* .

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

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

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

* refactor

* .
2025-08-07 20:53:21 +07:00
19 changed files with 518 additions and 607 deletions

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 }}

126
Cargo.lock generated
View File

@@ -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",
]
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
]
@@ -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",

View File

@@ -4,12 +4,12 @@ members = ["crates/*"]
default-members = ["crates/coop"]
[workspace.package]
version = "0.2.1"
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"

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.1"
version = "0.2.2"
out-dir = "../../dist"
before-packaging-command = "cargo build --release"
resources = ["Cargo.toml", "src"]

View File

@@ -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",

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| {

View File

@@ -9,7 +9,7 @@ use global::constants::{
APP_ID, APP_NAME, BOOTSTRAP_RELAYS, METADATA_BATCH_LIMIT, METADATA_BATCH_TIMEOUT,
SEARCH_RELAYS, WAIT_FOR_FINISH,
};
use global::{gift_wrap_sub_id, nostr_client, starting_time, 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,
@@ -50,9 +50,7 @@ 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::bounded::<Event>(2048);
let signal_tx_clone = signal_tx.clone();
let mta_tx_clone = mta_tx.clone();
app.background_executor()
.spawn(async move {
@@ -64,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}");
}
})
@@ -75,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
@@ -102,20 +99,23 @@ fn main() {
match smol::future::or(recv(), timeout()).await {
BatchEvent::NewKeys(public_key) => {
// Prevent duplicate keys from being processed
if processed_pubkeys.insert(public_key) {
batch.insert(public_key);
// Process immediately if batch limit reached
}
// 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;
}
@@ -243,7 +243,7 @@ fn main() {
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
@@ -267,15 +267,6 @@ fn main() {
}
});
}
// Load chat rooms without setting as finished
NostrSignal::Eose(subscription_id) => {
// Only load chat rooms if the subscription matches the gift wrap subscription
if gift_wrap_sub_id() == &subscription_id {
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| {
@@ -284,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);
}
@@ -356,32 +352,93 @@ 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 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, .. } => {
if processed_events.contains(&event.id) {
let RelayMessage::Event { event, .. } = message else {
continue;
};
// Skip events that have already been processed
if !processed_events().write().await.insert(event.id) {
continue;
}
// Skip events that have already been processed
processed_events.insert(event.id);
match event.kind {
Kind::GiftWrap => {
event_tx.send(event.into_owned()).await.ok();
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);
client
.subscribe_with_id(sub_id, filter, Some(auto_close))
.await
.ok();
}
}
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
@@ -389,34 +446,8 @@ async fn handle_nostr_notifications(
.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()
.author(event.pubkey)
.kind(Kind::InboxRelays)
.limit(1);
client.subscribe(filter, Some(opts)).await.ok();
}
_ => {}
}
}
RelayMessage::EndOfStoredEvents(subscription_id) => {
signal_tx
.send(NostrSignal::Eose(subscription_id.into_owned()))
.await?;
Kind::GiftWrap => {
event_tx.send(event.into_owned()).await.ok();
}
_ => {}
}
@@ -425,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

View File

@@ -9,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;
@@ -140,15 +140,7 @@ impl Chat {
// 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(),
@@ -720,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()
@@ -872,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()

View File

@@ -1,7 +1,7 @@
use std::time::Duration;
use anyhow::{anyhow, Error};
use global::constants::{GIFT_WRAP_SUB_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 builder = EventBuilder::new(Kind::InboxRelays, "").tags(
relays
let tags: Vec<Tag> = relays
.iter()
.map(|relay| Tag::relay(relay.clone()))
.collect_vec(),
);
.collect();
let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags);
// Set messaging relays
client.send_event_builder(builder).await?;
@@ -208,15 +203,6 @@ impl MessagingRelays {
_ = client.connect_relay(&relay).await;
}
let id = SubscriptionId::new(GIFT_WRAP_SUB_ID);
let new_messages = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
// Close old subscriptions
client.unsubscribe(&id).await;
// Subscribe to new messages
client.subscribe_with_id(id, new_messages, None).await?;
Ok(())
});
@@ -224,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();

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| {

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,
)
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,35 +677,21 @@ impl Render for Sidebar {
.overflow_y_hidden()
.child(
div()
.flex_none()
.px_1()
.w_full()
.h_9()
.flex()
.items_center()
.justify_between()
.child(
div()
.flex()
.items_center()
.h_flex()
.gap_2()
.flex_none()
.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| {
.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),
div().size_1().rounded_full().bg(cx.theme().cursor),
)
})
},
)
})
.small()
.bold()
.secondary()
@@ -764,19 +705,13 @@ impl Render for Sidebar {
Button::new("requests")
.label(t!("sidebar.requests_button"))
.tooltip(t!("sidebar.requests_tooltip"))
.when_some(
self.indicator.read(cx).as_ref(),
|this, kind| {
.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),
div().size_1().rounded_full().bg(cx.theme().cursor),
)
})
},
)
})
.small()
.bold()
.secondary()
@@ -786,22 +721,11 @@ impl Render for Sidebar {
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)
})),
),
),
)
})
}
}

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,14 +35,11 @@ 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 all gift wraps subscription.
pub const GIFT_WRAP_SUB_ID: &str = "listen_for_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;
@@ -52,7 +48,7 @@ pub const WAIT_FOR_FINISH: u64 = 60;
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";

View File

@@ -1,11 +1,12 @@
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::constants::GIFT_WRAP_SUB_ID;
use crate::paths::support_dir;
pub mod constants;
@@ -26,15 +27,15 @@ 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 GIFT_WRAP_ID: OnceLock<SubscriptionId> = 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();
@@ -50,22 +51,20 @@ 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 gift_wrap_sub_id() -> &'static SubscriptionId {
GIFT_WRAP_ID.get_or_init(|| SubscriptionId::new(GIFT_WRAP_SUB_ID))
pub fn processed_events() -> &'static RwLock<BTreeSet<EventId>> {
PROCESSED_EVENTS.get_or_init(|| RwLock::new(BTreeSet::new()))
}
pub fn starting_time() -> &'static Timestamp {

View File

@@ -4,7 +4,7 @@ use anyhow::{anyhow, Error};
use client_keys::ClientKeys;
use common::handle_auth::CoopAuthUrlHandler;
use global::constants::{ACCOUNT_D, NIP17_RELAYS, NIP65_RELAYS, NOSTR_CONNECT_TIMEOUT};
use global::{gift_wrap_sub_id, nostr_client};
use global::nostr_client;
use gpui::prelude::FluentBuilder;
use gpui::{
div, red, App, AppContext, Context, Entity, Global, ParentElement, SharedString, Styled,
@@ -29,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)]
@@ -73,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,
@@ -127,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?;
@@ -256,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| {
@@ -270,7 +272,7 @@ impl Identity {
this.verify_keys(enc, password, weak_error, window, cx);
})
.ok();
// false to keep the modal open
false
})
.child(
@@ -320,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 {
match task.await {
Ok(secret) => {
cx.update(|window, cx| {
window.close_modal(cx);
// Update user's signer with decrypted secret key
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();
} else {
_ = error.update(cx, |this, cx| {
*this = Some("Invalid password".into());
}
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,
{
@@ -357,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)
});
@@ -422,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)
});
@@ -547,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
@@ -613,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
@@ -622,45 +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> {
async fn get_nip65_relays(public_key: PublicKey) -> Result<(), Error> {
let client = nostr_client();
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
client
.subscribe_with_id(
gift_wrap_sub_id().to_owned(),
Filter::new().kind(Kind::GiftWrap).pubkey(public_key),
None,
)
.await?;
client
.subscribe(
Filter::new()
let sub_id = SubscriptionId::new("nip65-relays");
let filter = Filter::new()
.kind(Kind::RelayList)
.author(public_key)
.kinds(vec![Kind::Metadata, Kind::ContactList, Kind::RelayList])
.since(Timestamp::now()),
None,
)
.await?;
.limit(1);
client
.subscribe(
Filter::new()
.kinds(vec![Kind::Metadata, Kind::ContactList, Kind::RelayList])
.author(public_key)
.limit(10),
Some(opts),
)
.await?;
log::info!("Getting all user's metadata and messages...");
client.subscribe_with_id(sub_id, filter, Some(opts)).await?;
Ok(())
}
}

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>>,

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();
}
})

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);
}
});

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."

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}