diff --git a/Cargo.lock b/Cargo.lock index b541f88e..f66c443d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1129,7 +1129,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263" +source = "git+https://github.com/zed-industries/zed#5922f4adce07781467c0e3d172a11cf22fcfdab5" dependencies = [ "indexmap", "rustc-hash 2.1.1", @@ -1159,7 +1159,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f849b92c694fe237ecd8fafd1ba0df7ae0d45c1df6daeb7f68ed4220d51640bd" dependencies = [ "nix 0.30.1", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -1570,7 +1570,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263" +source = "git+https://github.com/zed-industries/zed#5922f4adce07781467c0e3d172a11cf22fcfdab5" dependencies = [ "proc-macro2", "quote", @@ -2506,7 +2506,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263" +source = "git+https://github.com/zed-industries/zed#5922f4adce07781467c0e3d172a11cf22fcfdab5" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2575,7 +2575,7 @@ dependencies = [ "strum 0.27.2", "sum_tree", "taffy", - "thiserror 2.0.16", + "thiserror 2.0.17", "usvg", "util", "util_macros", @@ -2600,7 +2600,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263" +source = "git+https://github.com/zed-industries/zed#5922f4adce07781467c0e3d172a11cf22fcfdab5" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2612,7 +2612,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263" +source = "git+https://github.com/zed-industries/zed#5922f4adce07781467c0e3d172a11cf22fcfdab5" dependencies = [ "anyhow", "gpui", @@ -2832,7 +2832,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263" +source = "git+https://github.com/zed-industries/zed#5922f4adce07781467c0e3d172a11cf22fcfdab5" dependencies = [ "anyhow", "bytes", @@ -2852,7 +2852,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263" +source = "git+https://github.com/zed-industries/zed#5922f4adce07781467c0e3d172a11cf22fcfdab5" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3657,7 +3657,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263" +source = "git+https://github.com/zed-industries/zed#5922f4adce07781467c0e3d172a11cf22fcfdab5" dependencies = [ "anyhow", "bindgen 0.71.1", @@ -3804,7 +3804,7 @@ dependencies = [ "rustc-hash 1.1.0", "spirv", "strum 0.26.3", - "thiserror 2.0.16", + "thiserror 2.0.17", "unicode-ident", ] @@ -3905,7 +3905,7 @@ dependencies = [ [[package]] name = "nostr" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#75345c65c645d33d15f6a5ec10fa6ffd0d8f4cac" +source = "git+https://github.com/rust-nostr/nostr#a5b37e2bc510b609c20e3ea28b1f3892a186241c" dependencies = [ "aes", "base64", @@ -3929,7 +3929,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#75345c65c645d33d15f6a5ec10fa6ffd0d8f4cac" +source = "git+https://github.com/rust-nostr/nostr#a5b37e2bc510b609c20e3ea28b1f3892a186241c" dependencies = [ "async-utility", "nostr", @@ -3941,7 +3941,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#75345c65c645d33d15f6a5ec10fa6ffd0d8f4cac" +source = "git+https://github.com/rust-nostr/nostr#a5b37e2bc510b609c20e3ea28b1f3892a186241c" dependencies = [ "flatbuffers", "lru", @@ -3952,7 +3952,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#75345c65c645d33d15f6a5ec10fa6ffd0d8f4cac" +source = "git+https://github.com/rust-nostr/nostr#a5b37e2bc510b609c20e3ea28b1f3892a186241c" dependencies = [ "async-utility", "flume", @@ -3966,7 +3966,7 @@ dependencies = [ [[package]] name = "nostr-relay-pool" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#75345c65c645d33d15f6a5ec10fa6ffd0d8f4cac" +source = "git+https://github.com/rust-nostr/nostr#a5b37e2bc510b609c20e3ea28b1f3892a186241c" dependencies = [ "async-utility", "async-wsocket", @@ -3983,7 +3983,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.43.0" -source = "git+https://github.com/rust-nostr/nostr#75345c65c645d33d15f6a5ec10fa6ffd0d8f4cac" +source = "git+https://github.com/rust-nostr/nostr#a5b37e2bc510b609c20e3ea28b1f3892a186241c" dependencies = [ "async-utility", "nostr", @@ -4500,7 +4500,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263" +source = "git+https://github.com/zed-industries/zed#5922f4adce07781467c0e3d172a11cf22fcfdab5" dependencies = [ "collections", "serde", @@ -4846,7 +4846,7 @@ dependencies = [ "rustc-hash 2.1.1", "rustls", "socket2", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -4867,7 +4867,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -4889,9 +4889,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] @@ -5087,18 +5087,18 @@ dependencies = [ [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", @@ -5108,7 +5108,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263" +source = "git+https://github.com/zed-industries/zed#5922f4adce07781467c0e3d172a11cf22fcfdab5" dependencies = [ "derive_refineable", "workspace-hack", @@ -5262,7 +5262,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263" +source = "git+https://github.com/zed-industries/zed#5922f4adce07781467c0e3d172a11cf22fcfdab5" dependencies = [ "anyhow", "bytes", @@ -5317,7 +5317,7 @@ dependencies = [ [[package]] name = "rope" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263" +source = "git+https://github.com/zed-industries/zed#5922f4adce07781467c0e3d172a11cf22fcfdab5" dependencies = [ "arrayvec", "log", @@ -5512,7 +5512,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.5.0", + "security-framework 3.5.1", ] [[package]] @@ -5549,7 +5549,7 @@ dependencies = [ "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki", - "security-framework 3.5.0", + "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs 0.26.11", "windows-sys 0.59.0", @@ -5782,9 +5782,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.5.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc198e42d9b7510827939c9a15f5062a0c913f3371d765977e586d2fe6c16f4a" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ "bitflags 2.9.4", "core-foundation 0.10.1", @@ -5812,7 +5812,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" [[package]] name = "semantic_version" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263" +source = "git+https://github.com/zed-industries/zed#5922f4adce07781467c0e3d172a11cf22fcfdab5" dependencies = [ "anyhow", "serde", @@ -6264,7 +6264,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263" +source = "git+https://github.com/zed-industries/zed#5922f4adce07781467c0e3d172a11cf22fcfdab5" dependencies = [ "arrayvec", "log", @@ -6585,11 +6585,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -6605,9 +6605,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -7084,7 +7084,7 @@ dependencies = [ "rustls", "rustls-pki-types", "sha1", - "thiserror 2.0.16", + "thiserror 2.0.17", "utf-8", ] @@ -7310,7 +7310,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263" +source = "git+https://github.com/zed-industries/zed#5922f4adce07781467c0e3d172a11cf22fcfdab5" dependencies = [ "anyhow", "async-fs", @@ -7345,7 +7345,7 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#78098f6809346ba8b45db7ffa768fba75578a263" +source = "git+https://github.com/zed-industries/zed#5922f4adce07781467c0e3d172a11cf22fcfdab5" dependencies = [ "perf", "quote", @@ -7833,7 +7833,7 @@ checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24" dependencies = [ "parking_lot", "rayon", - "thiserror 2.0.16", + "thiserror 2.0.17", "windows 0.61.3", "windows-future", ] @@ -8661,9 +8661,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" dependencies = [ "zeroize_derive", ] diff --git a/crates/coop/src/views/chat/mod.rs b/crates/coop/src/views/chat/mod.rs index ed7846f4..a8cc88a9 100644 --- a/crates/coop/src/views/chat/mod.rs +++ b/crates/coop/src/views/chat/mod.rs @@ -1,4 +1,5 @@ use std::collections::{HashMap, HashSet}; +use std::time::Duration; use common::display::{RenderedProfile, RenderedTimestamp}; use common::nip96::nip96_upload; @@ -99,7 +100,7 @@ impl Chat { let messages = BTreeSet::from([Message::system()]); let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.)); - let connect_relays = room.read(cx).connect_relays(cx); + let connect = room.read(cx).connect(cx); let load_messages = room.read(cx).load_messages(cx); let mut subscriptions = smallvec![]; @@ -108,43 +109,41 @@ impl Chat { tasks.push( // Load all messages belonging to this room cx.spawn_in(window, async move |this, cx| { - match connect_relays.await { - Ok(relays) => { - this.update(cx, |this, cx| { - this.relays.update(cx, |this, cx| { - *this = relays; - cx.notify(); - }); - }) - .ok(); - } - Err(e) => { - cx.update(|window, cx| { + let result = load_messages.await; + + this.update_in(cx, |this, window, cx| { + match result { + Ok(events) => { + this.insert_messages(events, cx); + } + Err(e) => { window.push_notification(e.to_string(), cx); - }) - .ok(); - } - }; + } + }; + }) + .ok(); }), ); tasks.push( - // Load all messages belonging to this room + // Get messaging relays for all members cx.spawn_in(window, async move |this, cx| { - match load_messages.await { - Ok(events) => { - this.update(cx, |this, cx| { - this.insert_messages(events, cx); - }) - .ok(); - } - Err(e) => { - cx.update(|window, cx| { - window.push_notification(e.to_string(), cx); - }) - .ok(); - } - }; + let result = connect.await; + + this.update_in(cx, |this, _window, cx| { + match result { + Ok(relays) => { + this.relays.update(cx, |this, cx| { + this.extend(relays); + cx.notify(); + }); + } + Err(e) => { + this.insert_warning(e.to_string(), cx); + } + }; + }) + .ok(); }), ); @@ -192,9 +191,12 @@ impl Chat { subscriptions.push( // Observe the messaging relays of the room's members cx.observe_in(&relays, window, |this, entity, _window, cx| { - for (public_key, urls) in entity.read(cx).clone().into_iter() { + let registry = Registry::global(cx); + let relays = entity.read(cx).clone(); + + for (public_key, urls) in relays.iter() { if urls.is_empty() { - let profile = Registry::read_global(cx).get_person(&public_key, cx); + let profile = registry.read(cx).get_person(public_key, cx); let content = t!("chat.nip17_not_found", u = profile.name()); this.insert_warning(content, cx); @@ -206,7 +208,6 @@ impl Chat { subscriptions.push( // Observe when user close chat panel cx.on_release_in(window, move |this, window, cx| { - this.disconnect_relays(cx); this.messages.clear(); this.rendered_texts_by_id.clear(); this.reports_by_id.clear(); @@ -235,20 +236,6 @@ impl Chat { } } - /// Disconnect all relays when the user closes the chat panel - fn disconnect_relays(&mut self, cx: &mut App) { - let relays = self.relays.read(cx).clone(); - - cx.background_spawn(async move { - let client = nostr_client(); - - for relay in relays.values().flatten() { - client.disconnect_relay(relay).await.ok(); - } - }) - .detach(); - } - /// Load all messages belonging to this room fn load_messages(&mut self, window: &mut Window, cx: &mut Context) { let load_messages = self.room.read(cx).load_messages(cx); @@ -273,11 +260,6 @@ impl Chat { ); } - #[allow(dead_code)] - fn mention_popup(&mut self, _text: &str, _input: &Entity, _cx: &mut Context) { - // TODO: open mention popup at current cursor position - } - /// Get user input content and merged all attachments fn input_content(&self, cx: &Context) -> String { let mut content = self.input.read(cx).value().trim().to_string(); @@ -313,36 +295,48 @@ impl Chat { return; } + // Temporary disable the message input + self.input.update(cx, |this, cx| { + this.set_loading(false, cx); + this.set_disabled(false, cx); + this.set_value("", window, cx); + }); + // Get the backup setting let backup = AppSettings::get_backup_messages(cx); // Get replies_to if it's present - let replies = self.replies_to.read(cx).iter().copied().collect_vec(); + let replies: Vec = self.replies_to.read(cx).iter().copied().collect(); // Get the current room entity let room = self.room.read(cx); - let identity = Registry::read_global(cx).identity(cx).public_key(); // Create a temporary message for optimistic update - let temp_message = room.create_temp_message(identity, &content, replies.as_ref()); - let temp_id = temp_message.id.unwrap(); + let rumor = room.create_message(&content, replies.as_ref(), cx); + let rumor_id = rumor.id.unwrap(); // Create a task for sending the message in the background - let send_message = room.send_in_background(&content, replies, backup, cx); + let send_message = room.send_message(rumor.clone(), backup, cx); // Optimistically update message list - self.insert_message(Message::user(temp_message), true, cx); + cx.spawn_in(window, async move |this, cx| { + cx.background_executor() + .timer(Duration::from_millis(100)) + .await; - // Remove all replies - self.remove_all_replies(cx); - - // remove all attachments - self.remove_all_attachments(cx); - - // Reset the input state - self.input.update(cx, |this, cx| { - this.set_value("", window, cx); - }); + this.update_in(cx, |this, window, cx| { + this.insert_message(Message::user(rumor), true, cx); + this.remove_all_replies(cx); + this.remove_all_attachments(cx); + this.input.update(cx, |this, cx| { + this.set_loading(false, cx); + this.set_disabled(false, cx); + this.set_value("", window, cx); + }); + }) + .ok(); + }) + .detach(); // Continue sending the message in the background cx.spawn_in(window, async move |this, cx| { @@ -363,7 +357,7 @@ impl Chat { }); // Insert the sent reports - this.reports_by_id.insert(temp_id, reports); + this.reports_by_id.insert(rumor_id, reports); cx.notify(); } @@ -377,37 +371,31 @@ impl Chat { .detach(); } + /// Resend a failed message fn resend_message(&mut self, id: &EventId, window: &mut Window, cx: &mut Context) { if let Some(reports) = self.reports_by_id.get(id).cloned() { - if let Some(message) = self.message(id) { - let backup = AppSettings::get_backup_messages(cx); - let id_clone = id.to_owned(); - let message = message.content.to_owned(); - let task = self.room.read(cx).resend(reports, message, backup, cx); + let id_clone = id.to_owned(); + let resend = self.room.read(cx).resend_message(reports, cx); - cx.spawn_in(window, async move |this, cx| { - match task.await { + cx.spawn_in(window, async move |this, cx| { + let result = resend.await; + + this.update_in(cx, |this, window, cx| { + match result { Ok(reports) => { - if !reports.is_empty() { - this.update(cx, |this, cx| { - this.reports_by_id.entry(id_clone).and_modify(|this| { - *this = reports; - }); - cx.notify(); - }) - .ok(); - } + this.reports_by_id.entry(id_clone).and_modify(|this| { + *this = reports; + }); + cx.notify(); } Err(e) => { - cx.update(|window, cx| { - window.push_notification(e.to_string(), cx); - }) - .ok(); + window.push_notification(Notification::error(e.to_string()), cx); } }; }) - .detach(); - } + .ok(); + }) + .detach(); } } @@ -612,7 +600,7 @@ impl Chat { }); } - fn render_announcement(&mut self, ix: usize, cx: &mut Context) -> AnyElement { + fn render_announcement(&self, ix: usize, cx: &Context) -> AnyElement { v_flex() .id(ix) .group("") @@ -638,7 +626,7 @@ impl Chat { .into_any_element() } - fn render_warning(&mut self, ix: usize, content: String, cx: &mut Context) -> AnyElement { + fn render_warning(&self, ix: usize, content: SharedString, cx: &Context) -> AnyElement { div() .id(ix) .relative() @@ -652,7 +640,7 @@ impl Chat { .text_sm() .text_color(cx.theme().warning_foreground) .child(Avatar::new("brand/system.png").size(rems(2.))) - .child(SharedString::from(content)), + .child(content), ) .child( div() @@ -666,23 +654,6 @@ impl Chat { .into_any_element() } - fn render_message_not_found(&self, ix: usize, cx: &Context) -> AnyElement { - div() - .id(ix) - .w_full() - .py_1() - .px_3() - .child( - h_flex() - .gap_1() - .text_xs() - .text_color(cx.theme().danger_foreground) - .child(SharedString::from(ix.to_string())) - .child(shared_t!("chat.not_found")), - ) - .into_any_element() - } - fn render_message( &self, ix: usize, @@ -1237,8 +1208,7 @@ impl Chat { weak_view.read_with(cx, |this, cx| this.new_subject(cx)) { room.update(cx, |this, cx| { - this.subject = Some(subject); - cx.notify(); + this.set_subject(subject, cx); }) .ok(); } @@ -1381,13 +1351,13 @@ impl Render for Chat { this.render_message(ix, rendered, text, cx) } - Message::Warning(content, _) => { - this.render_warning(ix, content.to_owned(), cx) + Message::Warning(content, _timestamp) => { + this.render_warning(ix, SharedString::from(content), cx) } - Message::System(_) => this.render_announcement(ix, cx), + Message::System(_timestamp) => this.render_announcement(ix, cx), } } else { - this.render_message_not_found(ix, cx) + this.render_warning(ix, shared_t!("chat.not_found"), cx) } }), ) diff --git a/crates/global/src/lib.rs b/crates/global/src/lib.rs index a1f2251d..52454a7c 100644 --- a/crates/global/src/lib.rs +++ b/crates/global/src/lib.rs @@ -223,7 +223,7 @@ pub fn nostr_client() -> &'static Client { .automatic_authentication(false) .verify_subscriptions(false) .sleep_when_idle(SleepWhenIdle::Enabled { - timeout: Duration::from_secs(300), + timeout: Duration::from_secs(600), }); ClientBuilder::default().database(lmdb).opts(opts).build() diff --git a/crates/registry/src/lib.rs b/crates/registry/src/lib.rs index 831f4a08..e617386d 100644 --- a/crates/registry/src/lib.rs +++ b/crates/registry/src/lib.rs @@ -437,7 +437,7 @@ impl Registry { // Update room room.update(cx, |this, cx| { if is_new_event { - this.created_at(event.created_at, cx); + this.set_created_at(event.created_at, cx); } // Set this room is ongoing if the new message is from current user diff --git a/crates/registry/src/message.rs b/crates/registry/src/message.rs index 0f75e263..495f036a 100644 --- a/crates/registry/src/message.rs +++ b/crates/registry/src/message.rs @@ -14,8 +14,8 @@ impl Message { Self::User(user.into()) } - pub fn warning(content: String) -> Self { - Self::Warning(content, Timestamp::now()) + pub fn warning(content: impl Into) -> Self { + Self::Warning(content.into(), Timestamp::now()) } pub fn system() -> Self { diff --git a/crates/registry/src/room.rs b/crates/registry/src/room.rs index e413982c..6c6dd062 100644 --- a/crates/registry/src/room.rs +++ b/crates/registry/src/room.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use std::hash::{Hash, Hasher}; use std::time::Duration; -use anyhow::Error; +use anyhow::{anyhow, Error}; use common::display::RenderedProfile; use common::event::EventUtils; use global::constants::SEND_RETRY; @@ -17,9 +17,9 @@ use crate::Registry; #[derive(Debug, Clone)] pub struct SendReport { pub receiver: PublicKey, - pub tags: Option>, pub status: Option>, pub error: Option, + pub on_hold: Option, pub relays_not_found: bool, } @@ -29,13 +29,14 @@ impl SendReport { receiver, status: None, error: None, - tags: None, + on_hold: None, relays_not_found: false, } } - pub fn not_found(mut self) -> Self { - self.relays_not_found = true; + pub fn status(mut self, output: Output) -> Self { + self.status = Some(output); + self.relays_not_found = false; self } @@ -45,14 +46,13 @@ impl SendReport { self } - pub fn status(mut self, output: Output) -> Self { - self.status = Some(output); - self.relays_not_found = false; + pub fn on_hold(mut self, event: Event) -> Self { + self.on_hold = Some(event); self } - pub fn tags(mut self, tags: &Vec) -> Self { - self.tags = Some(tags.to_owned()); + pub fn not_found(mut self) -> Self { + self.relays_not_found = true; self } @@ -235,7 +235,7 @@ impl Room { self } - /// Set the room kind to ongoing + /// Sets this room is ongoing conversation pub fn set_ongoing(&mut self, cx: &mut Context) { if self.kind != RoomKind::Ongoing { self.kind = RoomKind::Ongoing; @@ -243,29 +243,29 @@ impl Room { } } - /// Checks if the room is a group chat - pub fn is_group(&self) -> bool { - self.members.len() > 2 - } - /// Updates the creation timestamp of the room - pub fn created_at(&mut self, created_at: impl Into, cx: &mut Context) { + pub fn set_created_at(&mut self, created_at: impl Into, cx: &mut Context) { self.created_at = created_at.into(); cx.notify(); } /// Updates the subject of the room - pub fn subject(&mut self, subject: String, cx: &mut Context) { + pub fn set_subject(&mut self, subject: String, cx: &mut Context) { self.subject = Some(subject); cx.notify(); } /// Updates the picture of the room - pub fn picture(&mut self, picture: String, cx: &mut Context) { + pub fn set_picture(&mut self, picture: String, cx: &mut Context) { self.picture = Some(picture); cx.notify(); } + /// Checks if the room is a group chat + pub fn is_group(&self) -> bool { + self.members.len() > 2 + } + /// Gets the display name for the room pub fn display_name(&self, cx: &App) -> SharedString { if let Some(subject) = self.subject.clone() { @@ -289,13 +289,13 @@ impl Room { /// Get the first member of the room. /// /// First member is always different from the current user. - pub(crate) fn first_member(&self, cx: &App) -> Profile { + fn first_member(&self, cx: &App) -> Profile { let registry = Registry::read_global(cx); registry.get_person(&self.members[0], cx) } /// Merge the names of the first two members of the room. - pub(crate) fn merged_name(&self, cx: &App) -> SharedString { + fn merged_name(&self, cx: &App) -> SharedString { let registry = Registry::read_global(cx); if self.is_group() { @@ -322,43 +322,71 @@ impl Room { } } - /// Connects to all members' messaging relays - pub fn connect_relays( - &self, - cx: &App, - ) -> Task>, Error>> { + /// Connects to all members's messaging relays + pub fn connect(&self, cx: &App) -> Task>, Error>> { let members = self.members.clone(); cx.background_spawn(async move { let client = nostr_client(); - let timeout = Duration::from_secs(3); + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + + let mut relays = HashMap::new(); let mut processed = HashSet::new(); - let mut relays: HashMap> = HashMap::new(); - if let Some((_, members)) = members.split_last() { - for member in members.iter() { - relays.insert(member.to_owned(), vec![]); + for member in members.into_iter() { + if member == public_key { + continue; + }; - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(member.to_owned()) - .limit(1); + relays.insert(member, vec![]); - if let Ok(mut stream) = client.stream_events(filter, timeout).await { - if let Some(event) = stream.next().await { - if processed.insert(event.id) { - let urls = nip17::extract_owned_relay_list(event).collect_vec(); - relays.entry(member.to_owned()).or_default().extend(urls); - } + let filter = Filter::new() + .kind(Kind::InboxRelays) + .author(member) + .limit(1); + + let mut stream = client + .stream_events(filter, Duration::from_secs(10)) + .await?; + + if let Some(event) = stream.next().await { + if processed.insert(event.id) { + let public_key = event.pubkey; + let urls: Vec = nip17::extract_owned_relay_list(event).collect(); + + // Check if at least one URL exists + if urls.is_empty() { + continue; } + + // Connect to relays + for url in urls.iter() { + client.add_relay(url).await?; + client.connect_relay(url).await?; + } + + relays.entry(public_key).and_modify(|v| v.extend(urls)); } } - }; + } Ok(relays) }) } + pub fn disconnect(&self, relays: Vec, cx: &App) -> Task> { + cx.background_spawn(async move { + let client = nostr_client(); + + for relay in relays.into_iter() { + client.disconnect_relay(relay).await?; + } + + Ok(()) + }) + } + /// Loads all messages for this room from the database pub fn load_messages(&self, cx: &App) -> Task, Error>> { let members = self.members.clone(); @@ -415,23 +443,48 @@ impl Room { }) } - /// Creates a temporary message for optimistic updates - /// - /// The event must not been published to relays. - pub fn create_temp_message( - &self, - receiver: PublicKey, - content: &str, - replies: &[EventId], - ) -> UnsignedEvent { - let builder = EventBuilder::private_msg_rumor(receiver, content); + /// Emits a new message signal to the current room + pub fn emit_message(&self, gift_wrap_id: EventId, event: Event, cx: &mut Context) { + cx.emit(RoomSignal::NewMessage((gift_wrap_id, Box::new(event)))); + } + + /// Emits a signal to refresh the current room's messages. + pub fn emit_refresh(&mut self, cx: &mut Context) { + cx.emit(RoomSignal::Refresh); + } + + /// Create a new message event (unsigned) + pub fn create_message(&self, content: &str, replies: &[EventId], cx: &App) -> UnsignedEvent { + let public_key = Registry::read_global(cx).identity(cx).public_key(); + let subject = self.subject.clone(); + let picture = self.picture.clone(); + let mut tags = vec![]; - // Add event reference if it's present (replying to another event) + // Add receivers + // + // NOTE: current user will be removed from the list of receivers + for member in self.members.iter() { + tags.push(Tag::public_key(member.to_owned())); + } + + // Add subject tag if it's present + if let Some(subject) = subject { + tags.push(Tag::from_standardized(TagStandard::Subject( + subject.to_string(), + ))); + } + + // Add picture tag if it's present + if let Some(picture) = picture { + tags.push(Tag::custom(TagKind::custom("picture"), vec![picture])); + } + + // Add reply/quote tag if replies.len() == 1 { tags.push(Tag::event(replies[0])) } else { - for id in replies.iter() { + for id in replies { tags.push(Tag::from_standardized(TagStandard::Quote { event_id: id.to_owned(), relay_url: None, @@ -440,26 +493,27 @@ impl Room { } } - let mut event = builder.tags(tags).build(receiver); + // Construct a direct message event + // + // WARNING: never send this event to relays + let mut event = EventBuilder::new(Kind::PrivateDirectMessage, content) + .tags(tags) + .build(public_key); - // Ensure event ID is set + // Generate event ID event.ensure_id(); event } - /// Create a task to sends a message to all members in the background - pub fn send_in_background( + /// Create a task to send a message to all room members + pub fn send_message( &self, - content: &str, - replies: Vec, + rumor: UnsignedEvent, backup: bool, cx: &App, ) -> Task, Error>> { - let content = content.to_owned(); - let subject = self.subject.clone(); - let picture = self.picture.clone(); - let mut public_keys = self.members.clone(); + let mut members = self.members.clone(); cx.background_spawn(async move { let app_state = app_state(); @@ -467,57 +521,26 @@ impl Room { let signer = client.signer().await?; let public_key = signer.get_public_key().await?; - let mut tags: Vec = public_keys - .iter() - .filter_map(|&this| { - if this != public_key { - Some(Tag::public_key(this)) - } else { - None - } - }) - .collect(); + // Remove the current user's public key from the list of receivers + // Current user will be handled separately + members.retain(|&pk| pk != public_key); - // Add event reference if it's present (replying to another event) - if replies.len() == 1 { - tags.push(Tag::event(replies[0])) - } else { - for id in replies.iter() { - tags.push(Tag::from_standardized(TagStandard::Quote { - event_id: id.to_owned(), - relay_url: None, - public_key: None, - })) - } - } + let mut reports: Vec = vec![]; - // Add subject tag if it's present - if let Some(subject) = subject { - tags.push(Tag::from_standardized(TagStandard::Subject( - subject.to_string(), - ))); - } + for receiver in members.into_iter() { + let rumor = rumor.clone(); + let event = EventBuilder::gift_wrap(&signer, &receiver, rumor, vec![]).await?; - // Add picture tag if it's present - if let Some(picture) = picture { - tags.push(Tag::custom(TagKind::custom("picture"), vec![picture])); - } + let Ok(relay_urls) = Self::messaging_relays(receiver).await else { + reports.push(SendReport::new(receiver).not_found()); + continue; + }; - // Remove the current public key from the list of receivers - public_keys.retain(|&pk| pk != public_key); - - // Stored all send errors - let mut reports = vec![]; - - for pubkey in public_keys.into_iter() { - match client - .send_private_msg(pubkey, &content, tags.clone()) - .await - { + match client.send_event_to(relay_urls, &event).await { Ok(output) => { let id = output.id().to_owned(); let auth_required = output.failed.iter().any(|m| m.1.starts_with("auth-")); - let report = SendReport::new(pubkey).status(output).tags(&tags); + let report = SendReport::new(receiver).status(output); if auth_required { // Wait for authenticated and resent event successfully @@ -526,7 +549,7 @@ impl Room { // Check if event was successfully resent if let Some(output) = ids.iter().find(|e| e.id() == &id).cloned() { - let output = SendReport::new(pubkey).status(output).tags(&tags); + let output = SendReport::new(receiver).status(output); reports.push(output); break; } @@ -544,33 +567,31 @@ impl Room { } } Err(e) => { - if let nostr_sdk::client::Error::PrivateMsgRelaysNotFound = e { - reports.push(SendReport::new(pubkey).not_found().tags(&tags)); - } else { - reports.push(SendReport::new(pubkey).error(e.to_string()).tags(&tags)); - } + reports.push(SendReport::new(receiver).error(e.to_string())); } } } + // Construct a gift wrap to back up to current user's owned messaging relays + let rumor = rumor.clone(); + let event = EventBuilder::gift_wrap(&signer, &public_key, rumor, vec![]).await?; + // Only send a backup message to current user if sent successfully to others if reports.iter().all(|r| r.is_sent_success()) && backup { - match client - .send_private_msg(public_key, &content, tags.clone()) - .await - { - Ok(output) => { - reports.push(SendReport::new(public_key).status(output).tags(&tags)); - } - Err(e) => { - if let nostr_sdk::client::Error::PrivateMsgRelaysNotFound = e { - reports.push(SendReport::new(public_key).not_found()); - } else { - reports - .push(SendReport::new(public_key).error(e.to_string()).tags(&tags)); + if let Ok(relay_urls) = Self::messaging_relays(public_key).await { + match client.send_event_to(relay_urls, &event).await { + Ok(output) => { + reports.push(SendReport::new(public_key).status(output)); + } + Err(e) => { + reports.push(SendReport::new(public_key).error(e.to_string())); } } + } else { + reports.push(SendReport::new(public_key).not_found()); } + } else { + reports.push(SendReport::new(public_key).on_hold(event)); } Ok(reports) @@ -578,19 +599,19 @@ impl Room { } /// Create a task to resend a failed message - pub fn resend( + pub fn resend_message( &self, reports: Vec, - message: String, - backup: bool, cx: &App, ) -> Task, Error>> { cx.background_spawn(async move { let client = nostr_client(); let mut resend_reports = vec![]; - let mut resend_tag = vec![]; for report in reports.into_iter() { + let receiver = report.receiver; + + // Process failed events if let Some(output) = report.status { let id = output.id(); let urls: Vec<&RelayUrl> = output.failed.keys().collect(); @@ -599,44 +620,68 @@ impl Room { for url in urls.into_iter() { let relay = client.pool().relay(url).await?; let id = relay.send_event(&event).await?; + let resent: Output = Output { val: id, success: HashSet::from([url.to_owned()]), failed: HashMap::new(), }; - resend_reports.push(SendReport::new(report.receiver).status(resent)); - } - - if let Some(tags) = report.tags { - resend_tag.extend(tags); + resend_reports.push(SendReport::new(receiver).status(resent)); } } } - } - // Only send a backup message to current user if sent successfully to others - if backup && !resend_reports.is_empty() { - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - let output = client - .send_private_msg(public_key, message, resend_tag) - .await?; - - resend_reports.push(SendReport::new(public_key).status(output)); + // Process the on hold event if it exists + if let Some(event) = report.on_hold { + if let Ok(relay_urls) = Self::messaging_relays(receiver).await { + match client.send_event_to(relay_urls, &event).await { + Ok(output) => { + resend_reports.push(SendReport::new(receiver).status(output)); + } + Err(e) => { + resend_reports.push(SendReport::new(receiver).error(e.to_string())); + } + } + } else { + resend_reports.push(SendReport::new(receiver).not_found()); + } + } } Ok(resend_reports) }) } - /// Emits a new message signal to the current room - pub fn emit_message(&self, gift_wrap_id: EventId, event: Event, cx: &mut Context) { - cx.emit(RoomSignal::NewMessage((gift_wrap_id, Box::new(event)))); - } + /// Gets messaging relays for public key + async fn messaging_relays(public_key: PublicKey) -> Result, Error> { + let client = nostr_client(); + let mut relay_urls = vec![]; - /// Emits a signal to refresh the current room's messages. - pub fn emit_refresh(&mut self, cx: &mut Context) { - cx.emit(RoomSignal::Refresh); + let filter = Filter::new() + .kind(Kind::InboxRelays) + .author(public_key) + .limit(1); + + if let Some(event) = client.database().query(filter).await?.first_owned() { + let urls: Vec = nip17::extract_owned_relay_list(event).collect(); + + // Check if at least one URL exists + if urls.is_empty() { + return Err(anyhow!("Not found")); + } + + // Connect to relays + for url in urls.iter() { + client.add_relay(url).await?; + client.connect_relay(url).await?; + } + + relay_urls.extend(urls); + } else { + return Err(anyhow!("Not found")); + } + + Ok(relay_urls) } }