From ce8f431aaa7876eec674689b16100d65188a2a3d Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Thu, 4 Jun 2026 16:06:16 +0700 Subject: [PATCH 01/12] add nip4e settings --- Cargo.lock | 31 ++++++++++++++-------------- crates/device/Cargo.toml | 1 + crates/device/src/lib.rs | 42 ++++++++++++++++++++++++++++---------- crates/settings/src/lib.rs | 12 +++++++++++ 4 files changed, 60 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5cd980b..540d77e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -327,7 +327,7 @@ dependencies = [ [[package]] name = "assets" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "gpui", @@ -661,7 +661,7 @@ dependencies = [ [[package]] name = "auto_update" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "common", @@ -1140,7 +1140,7 @@ dependencies = [ [[package]] name = "chat" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "common", @@ -1163,7 +1163,7 @@ dependencies = [ [[package]] name = "chat_ui" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "chat", @@ -1388,7 +1388,7 @@ dependencies = [ [[package]] name = "common" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "bech32", @@ -1485,7 +1485,7 @@ dependencies = [ [[package]] name = "coop" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "assets", @@ -1526,7 +1526,7 @@ dependencies = [ [[package]] name = "coop_web" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "assets", @@ -1851,7 +1851,7 @@ dependencies = [ [[package]] name = "device" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "common", @@ -1863,6 +1863,7 @@ dependencies = [ "person", "serde", "serde_json", + "settings", "smallvec", "smol", "state", @@ -5146,7 +5147,7 @@ dependencies = [ [[package]] name = "person" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "common", @@ -5940,7 +5941,7 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "relay_auth" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "common", @@ -6596,7 +6597,7 @@ dependencies = [ [[package]] name = "settings" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "common", @@ -6853,7 +6854,7 @@ dependencies = [ [[package]] name = "state" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "common", @@ -7203,7 +7204,7 @@ dependencies = [ [[package]] name = "theme" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "gpui", @@ -7340,7 +7341,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "title_bar" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "common", @@ -7733,7 +7734,7 @@ dependencies = [ [[package]] name = "ui" -version = "1.0.0-beta4" +version = "1.0.0-beta5" dependencies = [ "anyhow", "common", diff --git a/crates/device/Cargo.toml b/crates/device/Cargo.toml index 5b73a5e..4f4cf11 100644 --- a/crates/device/Cargo.toml +++ b/crates/device/Cargo.toml @@ -10,6 +10,7 @@ state = { path = "../state" } person = { path = "../person" } ui = { path = "../ui" } theme = { path = "../theme" } +settings = { path = "../settings" } gpui.workspace = true nostr-sdk.workspace = true diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index c5ab2c7..21cfd8f 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -11,6 +11,8 @@ use gpui::{ }; use nostr_sdk::prelude::*; use person::PersonRegistry; +use settings::AppSettings; +use smallvec::{SmallVec, smallvec}; use state::{Announcement, CLIENT_NAME, NostrRegistry, StateEvent, TIMEOUT}; use theme::ActiveTheme; use ui::avatar::Avatar; @@ -19,8 +21,6 @@ use ui::notification::{Notification, NotificationKind}; use ui::{Disableable, Sizable, StyledExt, WindowExtension, h_flex, v_flex}; const IDENTIFIER: &str = "coop:device"; -const MSG: &str = "You've requested an encryption key from another device. \ - Approve to allow Coop to share with it."; pub fn init(window: &mut Window, cx: &mut App) { DeviceRegistry::set_global(cx.new(|cx| DeviceRegistry::new(window, cx)), cx); @@ -67,7 +67,7 @@ pub struct DeviceRegistry { tasks: Vec>>, /// Event subscription - _subscription: Option, + _subscriptions: SmallVec<[Subscription; 2]>, } impl EventEmitter for DeviceRegistry {} @@ -86,14 +86,28 @@ impl DeviceRegistry { /// Create a new device registry instance fn new(window: &mut Window, cx: &mut Context) -> Self { let nostr = NostrRegistry::global(cx); + let settings = AppSettings::global(cx); + let is_nip4e_enabled = settings.read(cx).is_nip4e_enabled(); - // Subscribe to nostr state events - let subscription = cx.subscribe_in(&nostr, window, |this, _e, event, _window, cx| { - if event == &StateEvent::SignerSet { - this.set_initializing(true, cx); - this.get_announcement(cx); - }; - }); + let mut subscriptions = smallvec![]; + + subscriptions.push( + // Subscribe to nostr state events + cx.observe(&settings, move |this, settings, cx| { + if settings.read(cx).is_nip4e_enabled() { + this.get_announcement(cx); + }; + }), + ); + + subscriptions.push( + // Subscribe to nostr state events + cx.subscribe(&nostr, move |this, _e, event, cx| { + if event == &StateEvent::SignerSet && is_nip4e_enabled { + this.get_announcement(cx); + }; + }), + ); cx.defer_in(window, |this, window, cx| { this.handle_notifications(window, cx); @@ -103,7 +117,7 @@ impl DeviceRegistry { initializing: true, pending_request: false, tasks: vec![], - _subscription: Some(subscription), + _subscriptions: subscriptions, } } @@ -219,6 +233,9 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + // Show the loading state + self.set_initializing(true, cx); + let task: Task> = cx.background_spawn(async move { let signer = client.signer().context("Signer not found")?; let public_key = signer.get_public_key().await?; @@ -586,6 +603,9 @@ impl DeviceRegistry { /// Build a notification for the encryption request. fn notification(&self, event: Event, cx: &Context) -> Notification { + const MSG: &str = "You've requested an encryption key from another device. \ + Approve to allow Coop to share with it."; + let request = Announcement::from(&event); let persons = PersonRegistry::global(cx); let profile = persons.read(cx).get(&request.public_key(), cx); diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index d081d31..cd7f56f 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -40,6 +40,7 @@ setting_accessors! { pub theme_mode: ThemeMode, pub hide_avatar: bool, pub screening: bool, + pub encryption_key: bool, pub auth_mode: AuthMode, pub trusted_relays: HashSet, pub room_configs: HashMap, @@ -137,6 +138,11 @@ pub struct Settings { /// Enable screening for unknown chat requests pub screening: bool, + /// Enable decoupling encryption key + /// + /// NIP-4e + pub encryption_key: bool, + /// Authentication mode pub auth_mode: AuthMode, @@ -157,6 +163,7 @@ impl Default for Settings { theme_mode: ThemeMode::default(), hide_avatar: false, screening: true, + encryption_key: false, auth_mode: AuthMode::default(), trusted_relays: HashSet::default(), room_configs: HashMap::default(), @@ -301,6 +308,11 @@ impl AppSettings { } } + /// Check if decoupling encryption key is enabled + pub fn is_nip4e_enabled(&self) -> bool { + self.values.encryption_key + } + /// Check if the given relay is already authenticated pub fn trusted_relay(&self, url: &RelayUrl, _cx: &App) -> bool { self.values.trusted_relays.iter().any(|relay| { -- 2.49.1 From ef227032bb652f4c62f739b6c7703f02048bdace Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Thu, 4 Jun 2026 17:56:31 +0700 Subject: [PATCH 02/12] fix clippy --- crates/state/src/lib.rs | 61 +-- crates/ui/src/input/display_map/mod.rs | 10 +- .../ui/src/input/display_map/text_wrapper.rs | 362 +----------------- crates/ui/src/input/element.rs | 26 +- 4 files changed, 32 insertions(+), 427 deletions(-) diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index a20325c..83739dc 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -270,60 +270,19 @@ impl NostrRegistry { let key_path = self.key_dir.join(format!("{}.npub", npub)); let app_keys = self.app_keys.clone(); - if let Ok(payload) = std::fs::read_to_string(key_path) { - if !payload.is_empty() { - cx.background_spawn(async move { - let decrypted = app_keys.nip44_decrypt(&public_key, &payload).await?; - let secret = SecretKey::parse(&decrypted)?; - let keys = Keys::new(secret); + if let Ok(payload) = std::fs::read_to_string(key_path) + && !payload.is_empty() + { + return cx.background_spawn(async move { + let decrypted = app_keys.nip44_decrypt(&public_key, &payload).await?; + let secret = SecretKey::parse(&decrypted)?; + let keys = Keys::new(secret); - Ok(keys.into_nostr_signer()) - }) - } else { - self.get_secret_keyring(&npub, cx) - } - } else { - self.get_secret_keyring(&npub, cx) + Ok(keys.into_nostr_signer()) + }); } - } - /// Get the secret for a given npub in the OS credentials store. - #[deprecated = "Use get_secret instead"] - fn get_secret_keyring( - &self, - user: &str, - cx: &App, - ) -> Task, Error>> { - let read = cx.read_credentials(user); - let app_keys = self.app_keys.clone(); - - cx.background_spawn(async move { - let (_, secret) = read - .await - .map_err(|_| anyhow!("Failed to get signer. Please re-import the secret key"))? - .ok_or_else(|| anyhow!("Failed to get signer. Please re-import the secret key"))?; - - // Try to parse as a direct secret key first - if let Ok(secret_key) = SecretKey::from_slice(&secret) { - return Ok(Keys::new(secret_key).into_nostr_signer()); - } - - // Convert the secret into string - let sec = String::from_utf8(secret) - .map_err(|_| anyhow!("Failed to parse secret as UTF-8"))?; - - // Try to parse as a NIP-46 URI - let uri = - NostrConnectUri::parse(&sec).map_err(|_| anyhow!("Failed to parse NIP-46 URI"))?; - - let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT); - let mut nip46 = NostrConnect::new(uri, app_keys, timeout, None)?; - - // Set the auth URL handler - nip46.auth_url_handler(CoopAuthUrlHandler); - - Ok(nip46.into_nostr_signer()) - }) + Task::ready(Err(anyhow::anyhow!("No secret found"))) } /// Add a new npub to the keys directory diff --git a/crates/ui/src/input/display_map/mod.rs b/crates/ui/src/input/display_map/mod.rs index 41fcfc6..11738a5 100644 --- a/crates/ui/src/input/display_map/mod.rs +++ b/crates/ui/src/input/display_map/mod.rs @@ -1,12 +1,4 @@ -/// Display mapping system for Editor/Input. -/// -/// This module implements a layered display mapping architecture: -/// - **WrapMap**: Handles soft-wrapping (buffer → wrap rows) -/// - **FoldMap**: Handles folding (wrap rows → display rows) -/// - **DisplayMap**: Public facade for Editor/Input -/// -/// The goal is to provide a clean, unified API where Editor only needs to know -/// about `BufferPoint ↔ DisplayPoint` mapping, without worrying about internal wrap/fold complexity. +#[allow(clippy::module_inception)] mod display_map; mod fold_map; #[cfg(not(target_family = "wasm"))] diff --git a/crates/ui/src/input/display_map/text_wrapper.rs b/crates/ui/src/input/display_map/text_wrapper.rs index 99acd14..dadb133 100644 --- a/crates/ui/src/input/display_map/text_wrapper.rs +++ b/crates/ui/src/input/display_map/text_wrapper.rs @@ -1,8 +1,7 @@ use std::ops::Range; -use gpui::Half; use gpui::{ - App, Font, LineFragment, Pixels, Point, ShapedLine, Size, TextAlign, Window, point, px, + App, Font, Half, LineFragment, Pixels, Point, ShapedLine, Size, TextAlign, Window, point, px, size, }; use ropey::Rope; @@ -97,7 +96,7 @@ impl TextWrapper { /// Get the line item by row index. #[inline] pub(crate) fn line(&self, row: usize) -> Option<&LineItem> { - self.lines.iter().skip(row).next() + self.lines.get(row) } pub(crate) fn set_wrap_width(&mut self, wrap_width: Option, cx: &mut App) { @@ -228,7 +227,7 @@ impl TextWrapper { }); } - if self.lines.len() == 0 { + if self.lines.is_empty() { self.lines = new_lines; } else { self.lines.splice(rows_range, new_lines); @@ -246,7 +245,7 @@ impl TextWrapper { /// /// If the `text` is the same as the current text, do nothing. fn update_all(&mut self, text: &Rope, cx: &mut App) { - self.update(text, &(0..text.len()), &text, cx); + self.update(text, &(0..text.len()), text, cx); } /// Return display point (with soft wrap) from the given byte offset in the text. @@ -278,7 +277,8 @@ impl TextWrapper { // Otherwise return the eof of the line. let last_range = line.wrapped_lines.last().unwrap_or(&(0..0)); let ix = line.lines_len().saturating_sub(1); - return WrapDisplayPoint::new(wrapped_row + ix, ix, last_range.len()); + + WrapDisplayPoint::new(wrapped_row + ix, ix, last_range.len()) } /// Return byte offset in the text from the given display point (with soft wrap). @@ -301,7 +301,7 @@ impl TextWrapper { wrapped_row += line.lines_len(); } - return self.text.len(); + self.text.len() } pub(crate) fn display_point_to_point(&self, point: WrapDisplayPoint) -> TreeSitterPoint { @@ -580,351 +580,3 @@ impl LineLayout { } } } - -#[cfg(test)] -mod tests { - use super::*; - use std::rc::Rc; - - use gpui::{Boundary, FontFeatures, FontStyle, FontWeight, px}; - - #[test] - fn test_update() { - let font = gpui::Font { - family: "Arial".into(), - weight: FontWeight::default(), - style: FontStyle::Normal, - features: FontFeatures::default(), - fallbacks: None, - }; - - let mut wrapper = TextWrapper::new(font, px(14.), None); - let mut text = Rope::from( - "Hello, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。", - ); - - fn fake_wrap_line(_line: &str, _wrap_width: Pixels) -> Vec { - vec![] - } - - #[track_caller] - fn assert_wrapper_lines(text: &Rope, wrapper: &TextWrapper, expected_lines: &[&[&str]]) { - let mut actual_lines = vec![]; - let mut offset = 0; - for line in wrapper.lines.iter() { - actual_lines.push( - line.wrapped_lines - .iter() - .map(|range| text.slice(offset + range.start..offset + range.end)) - .collect::>(), - ); - // +1 \n - offset += line.len() + 1; - } - assert_eq!(actual_lines, expected_lines); - } - - wrapper._update(&text, &(0..text.len()), &text, &mut fake_wrap_line); - assert_eq!(wrapper.lines.len(), 4); - assert_wrapper_lines( - &text, - &wrapper, - &[ - &["Hello, 世界!\r"], - &["This is second line."], - &["This is third line."], - &["这里是第 4 行。"], - ], - ); - - // Add a new text to end - let range = text.len()..text.len(); - let new_text = "New text"; - text.replace(range.clone(), new_text); - wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line); - assert_eq!( - text.to_string(), - "Hello, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。New text" - ); - assert_eq!(wrapper.lines.len(), 4); - assert_eq!(wrapper.lines.len(), 4); - assert_wrapper_lines( - &text, - &wrapper, - &[ - &["Hello, 世界!\r"], - &["This is second line."], - &["This is third line."], - &["这里是第 4 行。New text"], - ], - ); - - // Replace first line `Hello` to `AAA` - let range = 0..5; - let new_text = "AAA"; - text.replace(range.clone(), new_text); - wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line); - assert_eq!( - text.to_string(), - "AAA, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。New text" - ); - assert_eq!(wrapper.lines.len(), 4); - assert_wrapper_lines( - &text, - &wrapper, - &[ - &["AAA, 世界!\r"], - &["This is second line."], - &["This is third line."], - &["这里是第 4 行。New text"], - ], - ); - - // Remove the second line - let start_offset = text.line_start_offset(1); - let end_offset = text.line_end_offset(1); - let range = start_offset..end_offset + 1; - text.replace(range.clone(), ""); - wrapper._update(&text, &range, &Rope::from(""), &mut fake_wrap_line); - assert_eq!( - text.to_string(), - "AAA, 世界!\r\nThis is third line.\n这里是第 4 行。New text" - ); - assert_eq!(wrapper.lines.len(), 3); - assert_wrapper_lines( - &text, - &wrapper, - &[ - &["AAA, 世界!\r"], - &["This is third line."], - &["这里是第 4 行。New text"], - ], - ); - - // Replace the first 2 lines to "This is a new line." - let range = text.line_start_offset(0)..text.line_end_offset(1) + 1; - let new_text = "This is a new line.\nThis is new line 2.\n"; - text.replace(range.clone(), new_text); - wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line); - assert_eq!( - text.to_string(), - "This is a new line.\nThis is new line 2.\n这里是第 4 行。New text" - ); - assert_eq!(wrapper.lines.len(), 3); - assert_wrapper_lines( - &text, - &wrapper, - &[ - &["This is a new line."], - &["This is new line 2."], - &["这里是第 4 行。New text"], - ], - ); - - // Add a new line at the end - let range = text.len()..text.len(); - let new_text = "\nThis is a new line at the end."; - text.replace(range.clone(), new_text); - wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line); - assert_eq!( - text.to_string(), - "This is a new line.\nThis is new line 2.\n这里是第 4 行。New text\nThis is a new line at the end." - ); - assert_eq!(wrapper.lines.len(), 4); - assert_wrapper_lines( - &text, - &wrapper, - &[ - &["This is a new line."], - &["This is new line 2."], - &["这里是第 4 行。New text"], - &["This is a new line at the end."], - ], - ); - - // Add a new line at the beginning - let range = 0..0; - let new_text = "This is a new line at the beginning.\n"; - text.replace(range.clone(), new_text); - wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line); - assert_eq!( - text.to_string(), - "This is a new line at the beginning.\nThis is a new line.\nThis is new line 2.\n这里是第 4 行。New text\nThis is a new line at the end." - ); - assert_eq!(wrapper.lines.len(), 5); - assert_wrapper_lines( - &text, - &wrapper, - &[ - &["This is a new line at the beginning."], - &["This is a new line."], - &["This is new line 2."], - &["这里是第 4 行。New text"], - &["This is a new line at the end."], - ], - ); - - // Remove all to at least one line in `lines`. - let range = 0..text.len(); - let new_text = ""; - text.replace(range.clone(), new_text); - wrapper._update(&text, &range, &Rope::from(new_text), &mut fake_wrap_line); - assert_eq!(text.to_string(), ""); - assert_eq!(wrapper.lines.len(), 1); - assert_eq!(wrapper.lines[0].wrapped_lines, vec![0..0]); - - // Test update_all - let range = 0..text.len(); - let new_text = "This is a full text.\nThis is a second line."; - text.replace(range.clone(), new_text); - wrapper._update(&text, &range, &text, &mut fake_wrap_line); - assert_eq!( - text.to_string(), - "This is a full text.\nThis is a second line." - ); - assert_eq!(wrapper.lines.len(), 2); - } - - #[test] - fn test_line_layout() { - let mut line_layout = LineLayout::new(); - - let line1 = ShapedLine::default().with_len(100); - let line2 = ShapedLine::default().with_len(50); - let wrapped_lines = smallvec::smallvec![line1, line2]; - line_layout.set_wrapped_lines(wrapped_lines); - assert_eq!(line_layout.len(), 150); - assert_eq!(line_layout.wrapped_lines.len(), 2); - } - - #[test] - fn test_position_for_index_prefers_first_leading_empty_visual_line() { - let mut line_layout = LineLayout::new(); - line_layout.set_wrapped_lines(smallvec::smallvec![ - ShapedLine::default(), - ShapedLine::default(), - ShapedLine::default().with_len(3), - ]); - - let last_layout = LastLayout { - visible_range: 0..1, - visible_buffer_lines: vec![0], - visible_line_byte_offsets: vec![0], - visible_top: px(0.), - visible_range_offset: 0..0, - lines: Rc::new(vec![]), - line_height: px(20.), - wrap_width: None, - line_number_width: px(0.), - cursor_bounds: None, - text_align: TextAlign::Left, - content_width: px(0.), - }; - - assert_eq!( - line_layout.position_for_index(0, &last_layout, false), - Some(point(px(0.), px(0.))) - ); - } - - #[test] - fn test_offset_to_display_point() { - let font = gpui::Font { - family: "Arial".into(), - weight: FontWeight::default(), - style: FontStyle::Normal, - features: FontFeatures::default(), - fallbacks: None, - }; - - let mut wrapper = TextWrapper::new(font, px(14.), None); - wrapper.text = Rope::from( - "Hello, 世界!\r\nThis is second line.\nThis is third line.\n这里是第 4 行。", - ); - wrapper.lines = vec![ - // range: 0..15 - LineItem { - line: Rope::from("Hello, 世界!\r"), - wrapped_lines: vec![0..15], - }, - // range: 16..36 - LineItem { - line: Rope::from("This is second line."), - wrapped_lines: vec![0..10, 10..20], - }, - // range: 37..56 - LineItem { - line: Rope::from("This is third line."), - wrapped_lines: vec![0..9, 9..15, 15..20], - }, - // range: 57..79 - LineItem { - line: Rope::from("这里是第 4 行。"), - wrapped_lines: vec![0..22], - }, - ]; - - assert_eq!( - wrapper.offset_to_display_point(12), - WrapDisplayPoint::new(0, 0, 12) - ); - assert_eq!( - wrapper.offset_to_display_point(15), - WrapDisplayPoint::new(0, 0, 15) - ); - - assert_eq!( - wrapper.offset_to_display_point(16), - WrapDisplayPoint::new(1, 0, 0) - ); - assert_eq!( - wrapper.offset_to_display_point(21), - WrapDisplayPoint::new(1, 0, 5) - ); - assert_eq!( - wrapper.offset_to_display_point(27), - WrapDisplayPoint::new(2, 1, 1) - ); - assert_eq!( - wrapper.offset_to_display_point(37), - WrapDisplayPoint::new(3, 0, 0) - ); - assert_eq!( - wrapper.offset_to_display_point(54), - WrapDisplayPoint::new(5, 2, 2) - ); - assert_eq!( - wrapper.offset_to_display_point(59), - WrapDisplayPoint::new(6, 0, 2) - ); - - assert_eq!( - wrapper.display_point_to_offset(WrapDisplayPoint::new(6, 0, 2)), - 59 - ); - assert_eq!( - wrapper.display_point_to_offset(WrapDisplayPoint::new(5, 2, 2)), - 54 - ); - assert_eq!( - wrapper.display_point_to_offset(WrapDisplayPoint::new(3, 0, 0)), - 37 - ); - assert_eq!( - wrapper.display_point_to_offset(WrapDisplayPoint::new(2, 1, 1)), - 27 - ); - assert_eq!( - wrapper.display_point_to_offset(WrapDisplayPoint::new(1, 0, 5)), - 21 - ); - assert_eq!( - wrapper.display_point_to_offset(WrapDisplayPoint::new(1, 0, 0)), - 16 - ); - assert_eq!( - wrapper.display_point_to_offset(WrapDisplayPoint::new(0, 0, 15)), - 15 - ); - } -} diff --git a/crates/ui/src/input/element.rs b/crates/ui/src/input/element.rs index c53166e..8367e27 100644 --- a/crates/ui/src/input/element.rs +++ b/crates/ui/src/input/element.rs @@ -131,11 +131,14 @@ impl Element for EditorScrollbar { window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { - let mut style = Style::default(); - style.position = Position::Absolute; - style.size.width = relative(1.).into(); - style.size.height = relative(1.).into(); - + let style = Style { + position: Position::Absolute, + size: Size { + width: relative(1.).into(), + height: relative(1.).into(), + }, + ..Default::default() + }; (window.request_layout(style, [], cx), ()) } @@ -309,13 +312,12 @@ impl TextElement { let mut cursor_bounds = None; // If the input has a fixed height (Otherwise is auto-grow), we need to add a bottom margin to the input. - let top_bottom_margin = if state.mode.is_auto_grow() { - line_height - } else if visible_range.len() < BOTTOM_MARGIN_ROWS * 8 { - line_height - } else { - BOTTOM_MARGIN_ROWS * line_height - }; + let top_bottom_margin = + if state.mode.is_auto_grow() || visible_range.len() < BOTTOM_MARGIN_ROWS * 8 { + line_height + } else { + BOTTOM_MARGIN_ROWS * line_height + }; // The cursor corresponds to the current cursor position in the text no only the line. let mut cursor_pos = None; -- 2.49.1 From 2d3d90774ce96828abb7b2b9cb5a6e29e9e6a6de Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Fri, 5 Jun 2026 08:21:25 +0700 Subject: [PATCH 03/12] make the nip4e optional --- crates/chat_ui/src/lib.rs | 24 +++---- crates/settings/src/lib.rs | 2 +- crates/ui/src/input/input.rs | 2 +- desktop/src/workspace.rs | 135 ++++++++++++++++++----------------- 4 files changed, 84 insertions(+), 79 deletions(-) diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 310e75f..c2d2a6d 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -620,13 +620,19 @@ impl ChatPanel { }) .is_err() { - window.push_notification( - Notification::error("Failed to change subject").autohide(false), - cx, - ); + window.push_notification(Notification::error("Failed to change subject"), cx); } } Command::ChangeSigner(kind) => { + let is_nip4e_enabled = AppSettings::get_encryption_key(cx); + + if !is_nip4e_enabled + && (*kind == SignerKind::Encryption || *kind == SignerKind::Auto) + { + window.push_notification("Decoupling Encryption Key is not enabled", cx); + return; + } + if self .room .update(cx, |this, cx| { @@ -634,10 +640,7 @@ impl ChatPanel { }) .is_err() { - window.push_notification( - Notification::error("Failed to change signer").autohide(false), - cx, - ); + window.push_notification(Notification::error("Failed to change signer"), cx); } } Command::ToggleBackup => { @@ -648,10 +651,7 @@ impl ChatPanel { }) .is_err() { - window.push_notification( - Notification::error("Failed to toggle backup").autohide(false), - cx, - ); + window.push_notification(Notification::error("Failed to toggle backup"), cx); } } Command::Copy(public_key) => { diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index cd7f56f..9ad22e8 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -67,8 +67,8 @@ impl Display for AuthMode { /// Signer kind #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub enum SignerKind { - #[default] Auto, + #[default] User, Encryption, } diff --git a/crates/ui/src/input/input.rs b/crates/ui/src/input/input.rs index 68d4e1f..8187819 100644 --- a/crates/ui/src/input/input.rs +++ b/crates/ui/src/input/input.rs @@ -18,7 +18,7 @@ pub(crate) fn input_style(disabled: bool, cx: &App) -> (Hsla, Hsla) { if disabled { (cx.theme().surface_background, cx.theme().text_muted) } else { - (cx.theme().surface_background, cx.theme().text) + (cx.theme().elevated_surface_background, cx.theme().text) } } diff --git a/desktop/src/workspace.rs b/desktop/src/workspace.rs index 64f17f4..fce9d06 100644 --- a/desktop/src/workspace.rs +++ b/desktop/src/workspace.rs @@ -641,6 +641,7 @@ impl Workspace { let chat = ChatRegistry::global(cx); let initializing = chat.read(cx).initializing; let trash_messages = chat.read(cx).count_trash_messages(cx); + let is_nip4e_enabled = AppSettings::get_encryption_key(cx); let device = DeviceRegistry::global(cx); let device_initializing = device.read(cx).initializing; @@ -691,72 +692,76 @@ impl Workspace { }), ) }) - .child( - Button::new("key") - .icon(IconName::UserKey) - .tooltip("Decoupled encryption key") - .small() - .ghost() - .loading(device_initializing) - .when(device_initializing, |this| { - this.label("Dekey") - .xsmall() - .tooltip("Loading decoupled encryption key...") - }) - .dropdown_menu(move |this, _window, _cx| { - this.min_w(px(260.)) - .label("Encryption Key") - .when_some(announcement.as_ref(), |this, announcement| { - let name = announcement.client_name(); - let pkey = shorten_pubkey(announcement.public_key(), 8); + .when(is_nip4e_enabled, |this| { + this.child( + Button::new("key") + .icon(IconName::UserKey) + .tooltip("Decoupled encryption key") + .small() + .ghost() + .loading(device_initializing) + .when(device_initializing, |this| { + this.label("Dekey") + .xsmall() + .tooltip("Loading decoupled encryption key...") + }) + .dropdown_menu(move |this, _window, _cx| { + this.min_w(px(260.)) + .label("Encryption Key") + .when_some(announcement.as_ref(), |this, announcement| { + let name = announcement.client_name(); + let pkey = shorten_pubkey(announcement.public_key(), 8); - this.item(PopupMenuItem::element(move |_window, cx| { - h_flex() - .gap_1() - .text_sm() - .child( - Icon::new(IconName::Device) - .small() - .text_color(cx.theme().icon_muted), - ) - .child(name.clone()) - })) - .item(PopupMenuItem::element(move |_window, cx| { - h_flex() - .gap_1() - .text_sm() - .child( - Icon::new(IconName::UserKey) - .small() - .text_color(cx.theme().icon_muted), - ) - .child(SharedString::from(pkey.clone())) - })) - }) - .separator() - .menu_with_icon( - "Backup", - IconName::Shield, - Box::new(Command::BackupEncryption), - ) - .menu_with_icon( - "Restore from secret key", - IconName::Usb, - Box::new(Command::ImportEncryption), - ) - .separator() - .menu_with_icon( - "Reload", - IconName::Refresh, - Box::new(Command::RefreshEncryption), - ) - .menu_with_icon( - "Reset", - IconName::Warning, - Box::new(Command::ResetEncryption), - ) - }), - ) + this.item(PopupMenuItem::element(move |_window, cx| { + h_flex() + .gap_1() + .text_sm() + .child( + Icon::new(IconName::Device) + .small() + .text_color(cx.theme().icon_muted), + ) + .child(name.clone()) + })) + .item( + PopupMenuItem::element(move |_window, cx| { + h_flex() + .gap_1() + .text_sm() + .child( + Icon::new(IconName::UserKey) + .small() + .text_color(cx.theme().icon_muted), + ) + .child(SharedString::from(pkey.clone())) + }), + ) + }) + .separator() + .menu_with_icon( + "Backup", + IconName::Shield, + Box::new(Command::BackupEncryption), + ) + .menu_with_icon( + "Restore from secret key", + IconName::Usb, + Box::new(Command::ImportEncryption), + ) + .separator() + .menu_with_icon( + "Reload", + IconName::Refresh, + Box::new(Command::RefreshEncryption), + ) + .menu_with_icon( + "Reset", + IconName::Warning, + Box::new(Command::ResetEncryption), + ) + }), + ) + }) .child( Button::new("inbox") .icon(IconName::Inbox) -- 2.49.1 From a0d76e2cf43731b741d679acb9a01f1434da4019 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Fri, 5 Jun 2026 08:34:34 +0700 Subject: [PATCH 04/12] add nip4e settings --- crates/settings/src/lib.rs | 4 ++-- desktop/src/dialogs/settings.rs | 26 ++++++++++++++++++++------ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index 9ad22e8..e107994 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -68,9 +68,9 @@ impl Display for AuthMode { #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub enum SignerKind { Auto, + Encryption, #[default] User, - Encryption, } impl SignerKind { @@ -98,7 +98,7 @@ impl RoomConfig { pub fn new() -> Self { Self { backup: true, - signer_kind: SignerKind::Auto, + signer_kind: SignerKind::default(), } } diff --git a/desktop/src/dialogs/settings.rs b/desktop/src/dialogs/settings.rs index f6e67dc..408f0c6 100644 --- a/desktop/src/dialogs/settings.rs +++ b/desktop/src/dialogs/settings.rs @@ -56,17 +56,16 @@ impl Preferences { impl Render for Preferences { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - const SCREENING: &str = - "When opening a request, a popup will appear to help you identify the sender."; - const AVATAR: &str = - "Hide all avatar pictures to improve performance and protect your privacy."; - const MODE: &str = - "Choose whether to use the selected light or dark theme, or to follow the OS."; + const SCREENING: &str = "Show an screening dialog to verify the unknown sender."; + const AVATAR: &str = "Hide all avatar pictures to improve performance."; + const MODE: &str = "Use the selected light or dark theme, or to follow the OS."; + const NIP4E: &str = "Use a dedicated key to encrypt and decrypt messages."; const AUTH: &str = "Choose the authentication behavior for relays."; const RESET: &str = "Reset the theme to the default one."; let screening = AppSettings::get_screening(cx); let hide_avatar = AppSettings::get_hide_avatar(cx); + let nip4e = AppSettings::get_encryption_key(cx); let auth_mode = AppSettings::get_auth_mode(cx); let theme_mode = AppSettings::get_theme_mode(cx); @@ -207,6 +206,21 @@ impl Render for Preferences { ), ), ) + .child( + GroupBox::new() + .id("experiments") + .title("Experiments") + .fill() + .child( + Switch::new("nip4e") + .label("Decoupling Encryption Key") + .description(NIP4E) + .checked(nip4e) + .on_click(move |_, _window, cx| { + AppSettings::update_encryption_key(!nip4e, cx); + }), + ), + ) .child( GroupBox::new() .id("media") -- 2.49.1 From d53e9d538cd17ffb329c3617ac3ec89e78418c4e Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Fri, 5 Jun 2026 12:16:13 +0700 Subject: [PATCH 05/12] refactor --- crates/chat/src/lib.rs | 224 ++++++++--------------- crates/relay_auth/src/lib.rs | 33 ++-- crates/state/src/lib.rs | 14 +- crates/state/src/{device.rs => nip4e.rs} | 0 desktop/src/workspace.rs | 9 +- 5 files changed, 101 insertions(+), 179 deletions(-) rename crates/state/src/{device.rs => nip4e.rs} (100%) diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index a33dcf0..2c35522 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -7,7 +7,6 @@ use std::time::Duration; use anyhow::{Context as AnyhowContext, Error, anyhow}; use common::EventExt; -use device::{DeviceEvent, DeviceRegistry}; use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::skim::SkimMatcherV2; use gpui::{ @@ -49,6 +48,10 @@ pub enum ChatEvent { /// Channel signal. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] enum Signal { + /// Inbox Relays found, the app is ready to subscribe messages + InboxReady(Box), + /// No Inbox Relays found, the app is not ready to subscribe messages + InboxRelayNotFound, /// Message received from relay pool Message(NewMessage), /// Eose received from relay pool @@ -62,6 +65,14 @@ impl Signal { Self::Message(NewMessage::new(gift_wrap, rumor)) } + pub fn inbox_ready(event: &Event) -> Self { + Self::InboxReady(Box::new(event.to_owned())) + } + + pub fn inbox_relay_not_found() -> Self { + Self::InboxRelayNotFound + } + pub fn eose() -> Self { Self::Eose } @@ -80,9 +91,6 @@ type GiftWrapId = EventId; /// Chat Registry #[derive(Debug)] pub struct ChatRegistry { - /// Whether the chat registry is currently initializing. - pub initializing: bool, - /// Chat rooms rooms: Vec>, @@ -127,61 +135,29 @@ impl ChatRegistry { /// Create a new chat registry instance fn new(window: &mut Window, cx: &mut Context) -> Self { let nostr = NostrRegistry::global(cx); - let device = DeviceRegistry::global(cx); let (tx, rx) = flume::unbounded::(); let mut subscriptions = smallvec![]; subscriptions.push( // Subscribe to the signer event - cx.subscribe_in(&nostr, window, |this, state, event, window, cx| { + cx.subscribe(&nostr, |this, _state, event, cx| { if event == &StateEvent::SignerSet { this.reset(cx); - this.get_contact_list(cx); + this.get_metadata(cx); this.get_rooms(cx); - - let signer = state.read(cx).signer(); - cx.spawn_in(window, async move |this, cx| { - let user_signer = signer.get().await; - this.update(cx, |this, cx| { - this.get_messages(user_signer, cx); - }) - .ok(); - }) - .detach(); - }; - }), - ); - - subscriptions.push( - // Subscribe to the device event - cx.subscribe_in(&device, window, |_this, _s, event, window, cx| { - if event == &DeviceEvent::Set { - let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); - - cx.spawn_in(window, async move |this, cx| { - if let Some(device_signer) = signer.get_encryption_signer().await { - this.update(cx, |this, cx| { - this.get_messages(device_signer, cx); - }) - .ok(); - } - }) - .detach(); }; }), ); // Run at the end of the current cycle cx.defer_in(window, |this, _window, cx| { - this.get_rooms(cx); this.handle_notifications(cx); this.tracking(cx); + this.get_rooms(cx); }); Self { - initializing: true, rooms: vec![], trashes: cx.new(|_| BTreeSet::default()), seens: Arc::new(RwLock::new(HashMap::default())), @@ -238,6 +214,15 @@ impl ChatRegistry { continue; } + // Handle msg relays event to determine when the app is ready to subscribe + if event.kind == Kind::InboxRelays { + let current_user = signer.get_public_key().await?; + + if event.pubkey == current_user { + tx.send_async(Signal::inbox_ready(&event)).await?; + } + } + // Skip non-gift wrap events if event.kind != Kind::GiftWrap { continue; @@ -253,17 +238,9 @@ impl ChatRegistry { event_map.insert(rumor.id.unwrap(), (event.id, dekey)); } - if rumor.kind != Kind::PrivateDirectMessage - || rumor.kind != Kind::Custom(15) - { - log::info!("Rumor is not releated to NIP17"); - continue; - } - // Check if the rumor has a recipient if rumor.tags.is_empty() { - let signal = - Signal::error(event.as_ref(), "Recipient is missing"); + let signal = Signal::error(&event, "Recipient is missing"); tx.send_async(signal).await?; } @@ -303,6 +280,16 @@ impl ChatRegistry { this.new_message(message, cx); })?; } + Signal::InboxReady(event) => { + this.update(cx, |this, cx| { + this.get_messages(&event, cx); + })?; + } + Signal::InboxRelayNotFound => { + this.update(cx, |_this, cx| { + cx.emit(ChatEvent::Error("Messaging Relays not found".into())); + })?; + } Signal::Eose => { this.update(cx, |this, cx| { this.get_rooms(cx); @@ -342,7 +329,7 @@ impl ChatRegistry { } /// Get contact list from relays - fn get_contact_list(&mut self, cx: &mut Context) { + fn get_metadata(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); @@ -351,96 +338,63 @@ impl ChatRegistry { return; }; - let task: Task> = cx.background_spawn(async move { - let id = SubscriptionId::new("contact-list"); + self.tasks.push(cx.background_spawn(async move { let opts = SubscribeAutoCloseOptions::default() .exit_policy(ReqExitPolicy::ExitOnEOSE) .timeout(Some(Duration::from_secs(TIMEOUT))); - // Construct filter for inbox relays - let filter = Filter::new() + // Construct filter for msg relays + let msg_relays = Filter::new() + .kind(Kind::InboxRelays) + .author(public_key) + .limit(1); + + // Construct filter for contact list + let contact_list = Filter::new() .kind(Kind::ContactList) .author(public_key) .limit(1); // Subscribe - client.subscribe(filter).close_on(opts).with_id(id).await?; + client + .subscribe(vec![msg_relays, contact_list]) + .close_on(opts) + .await?; Ok(()) - }); + })); - self.tasks.push(task); - } + let client = nostr.read(cx).client(); + let tx = self.signal_tx.clone(); - /// Get all messages for the provided signer - fn get_messages(&mut self, signer: T, cx: &mut Context) - where - T: NostrSigner + 'static, - { - let task = self.subscribe_gift_wrap_events(signer, cx); + self.tasks.push(cx.background_spawn(async move { + loop { + let filter = Filter::new() + .kind(Kind::InboxRelays) + .author(public_key) + .limit(1); - self.tasks.push(cx.spawn(async move |this, cx| { - match task.await { - Ok(_) => { - this.update(cx, |this, cx| { - this.set_initializing(false, cx); - })?; - } - Err(e) => { - this.update(cx, |_this, cx| { - cx.emit(ChatEvent::Error(SharedString::from(e.to_string()))); - })?; + if client.database().query(filter).await?.first().is_some() { + break; + } else { + tx.send_async(Signal::inbox_relay_not_found()).await?; } + + smol::Timer::after(Duration::from_secs(5)).await; } + Ok(()) })); } - // Get messaging relay list for current user - fn get_messaging_relays(&self, cx: &App) -> Task, Error>> { + /// Get all messages for the provided signer + fn get_messages(&mut self, msg_relays: &Event, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); + let urls: Vec = nip17::extract_relay_list(msg_relays).cloned().collect(); - cx.background_spawn(async move { - let public_key = signer.get_public_key().await?; - let id = SubscriptionId::new("inbox-relay"); - - // Construct filter for inbox relays - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(public_key) - .limit(1); - - // Stream events from user's write relays - let mut stream = client - .stream_events(filter) - .with_id(id) - .timeout(Duration::from_secs(TIMEOUT)) - .await?; - - while let Some((_url, res)) = stream.next().await { - if let Ok(event) = res { - let urls: Vec = nip17::extract_owned_relay_list(event).collect(); - return Ok(urls); - } - } - - Err(anyhow!("Messaging Relays not found")) - }) - } - - /// Continuously get gift wrap events for the signer - fn subscribe_gift_wrap_events(&self, signer: T, cx: &App) -> Task> - where - T: NostrSigner + 'static, - { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - let urls = self.get_messaging_relays(cx); - - cx.background_spawn(async move { - let urls = urls.await?; + let task: Task> = cx.background_spawn(async move { let public_key = signer.get_public_key().await?; let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); let id = SubscriptionId::new(format!("{}-msg", public_key.to_hex())); @@ -464,38 +418,23 @@ impl ChatRegistry { ); Ok(()) - }) + }); + + self.tasks.push(cx.spawn(async move |this, cx| { + if let Err(e) = task.await { + this.update(cx, |_this, cx| { + cx.emit(ChatEvent::Error(SharedString::from(e.to_string()))); + })?; + } + Ok(()) + })); } /// Refresh the chat registry, fetching messages and contact list from relays. - pub fn refresh(&mut self, window: &mut Window, cx: &mut Context) { + pub fn refresh(&mut self, cx: &mut Context) { self.reset(cx); - self.get_contact_list(cx); + self.get_metadata(cx); self.get_rooms(cx); - - let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); - - cx.spawn_in(window, async move |this, cx| { - let user_signer = signer.get().await; - let device_signer = signer.get_encryption_signer().await; - - this.update(cx, |this, cx| { - this.get_messages(user_signer, cx); - - if let Some(device_signer) = device_signer { - this.get_messages(device_signer, cx); - } - }) - .ok(); - }) - .detach(); - } - - /// Set the initializing status of the chat registry - fn set_initializing(&mut self, initializing: bool, cx: &mut Context) { - self.initializing = initializing; - cx.notify(); } /// Get the loading status of the chat registry @@ -650,7 +589,6 @@ impl ChatRegistry { /// Reset the registry. pub fn reset(&mut self, cx: &mut Context) { - self.initializing = true; self.rooms.clear(); self.trashes.update(cx, |this, cx| { this.clear(); diff --git a/crates/relay_auth/src/lib.rs b/crates/relay_auth/src/lib.rs index 4bac09a..dc63cc7 100644 --- a/crates/relay_auth/src/lib.rs +++ b/crates/relay_auth/src/lib.rs @@ -217,8 +217,6 @@ impl RelayAuth { .send_msg(ClientMessage::Auth(Cow::Borrowed(&event))) .await?; - log::info!("Sending AUTH event"); - while let Some(notification) = notifications.next().await { match notification { RelayNotification::Message { message } => { @@ -272,29 +270,24 @@ impl RelayAuth { this.update_in(cx, |this, window, cx| { window.clear_notification_by_id::(challenge, cx); - match result { - Ok(_) => { - // Clear pending events for the authenticated relay - this.clear_pending_events(url, cx); + if let Err(e) = result { + window + .push_notification(Notification::error(e.to_string()).autohide(false), cx); + } else { + // Clear pending events for the authenticated relay + this.clear_pending_events(url, cx); + // Only show the success notification if the relay was not already trusted + if !settings.read(cx).trusted_relay(url, cx) { + let domain = url.domain().unwrap_or_default(); + let msg = format!("Relay {} has been authenticated", domain); + + window.push_notification(Notification::success(msg), cx); + } else { // Save the authenticated relay to automatically authenticate future requests settings.update(cx, |this, cx| { this.add_trusted_relay(url, cx); }); - - window.push_notification( - Notification::success(format!( - "Relay {} has been authenticated", - url.domain().unwrap_or_default() - )), - cx, - ); - } - Err(e) => { - window.push_notification( - Notification::error(e.to_string()).autohide(false), - cx, - ); } } }) diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 83739dc..2fc4c51 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -13,13 +13,13 @@ use nostr_sdk::prelude::*; mod blossom; mod constants; -mod device; mod nip05; +mod nip4e; mod signer; pub use blossom::*; pub use constants::*; -pub use device::*; +pub use nip4e::*; pub use nip05::*; pub use signer::*; @@ -149,11 +149,12 @@ impl NostrRegistry { // Run at the end of current cycle cx.defer_in(window, |this, _window, cx| { this.connect(cx); - // Create an identity if none exists + if this.npubs.read(cx).is_empty() { + // Create an identity if none exists this.create_identity(cx); } else { - // Show the identity dialog + // Show the account selector dialog cx.emit(StateEvent::Show); } }); @@ -234,10 +235,7 @@ impl NostrRegistry { } // Connect to all added relays - client - .connect() - .and_wait(Duration::from_secs(TIMEOUT)) - .await; + client.connect().await; Ok(()) }); diff --git a/crates/state/src/device.rs b/crates/state/src/nip4e.rs similarity index 100% rename from crates/state/src/device.rs rename to crates/state/src/nip4e.rs diff --git a/desktop/src/workspace.rs b/desktop/src/workspace.rs index fce9d06..f487b1b 100644 --- a/desktop/src/workspace.rs +++ b/desktop/src/workspace.rs @@ -353,7 +353,7 @@ impl Workspace { let chat = ChatRegistry::global(cx); // Trigger a refresh of the chat registry chat.update(cx, |this, cx| { - this.refresh(window, cx); + this.refresh(cx); }); } Command::ShowRelayList => { @@ -639,7 +639,6 @@ impl Workspace { fn titlebar_right(&mut self, cx: &mut Context) -> impl IntoElement { let chat = ChatRegistry::global(cx); - let initializing = chat.read(cx).initializing; let trash_messages = chat.read(cx).count_trash_messages(cx); let is_nip4e_enabled = AppSettings::get_encryption_key(cx); @@ -767,12 +766,6 @@ impl Workspace { .icon(IconName::Inbox) .small() .ghost() - .loading(initializing) - .when(initializing, |this| { - this.label("Inbox") - .xsmall() - .tooltip("Getting inbox messages...") - }) .dropdown_menu(move |this, _window, cx| { let urls: Vec<(SharedString, SharedString)> = profile .messaging_relays() -- 2.49.1 From c7913096595e589a4bc66d11911b8068f9743d5e Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Fri, 5 Jun 2026 13:31:30 +0700 Subject: [PATCH 06/12] . --- crates/chat/src/lib.rs | 84 ++++++++++++++++------------------- crates/chat_ui/src/lib.rs | 18 -------- crates/ui/src/notification.rs | 16 ++++--- desktop/src/workspace.rs | 22 +++++++++ 4 files changed, 71 insertions(+), 69 deletions(-) diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index 2c35522..f5346e7 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -16,7 +16,7 @@ use gpui::{ use nostr_sdk::prelude::*; use smallvec::{SmallVec, smallvec}; use smol::lock::RwLock; -use state::{CoopSigner, DEVICE_GIFTWRAP, NostrRegistry, StateEvent, TIMEOUT, USER_GIFTWRAP}; +use state::{CoopSigner, DEVICE_GIFTWRAP, NostrRegistry, StateEvent, USER_GIFTWRAP}; mod message; mod room; @@ -41,6 +41,8 @@ pub enum ChatEvent { CloseRoom(u64), /// An event to notify UI about a new chat request Ping, + /// No Inbox Relays found, the app is not ready to subscribe messages + InboxRelayNotFound, /// An error occurred Error(SharedString), } @@ -85,9 +87,6 @@ impl Signal { } } -type Dekey = bool; -type GiftWrapId = EventId; - /// Chat Registry #[derive(Debug)] pub struct ChatRegistry { @@ -101,10 +100,13 @@ pub struct ChatRegistry { seens: Arc>>>, /// Mapping of unwrapped event ids to their gift wrap event ids - event_map: Arc>>, + event_map: Arc>>, /// Tracking the status of unwrapping gift wrap events. - tracking_flag: Arc, + tracking: Arc, + + /// Whether the messaging relays have been found. + msg_relays_existed: Arc, /// Channel for sending signals to the UI. signal_tx: flume::Sender, @@ -162,7 +164,8 @@ impl ChatRegistry { trashes: cx.new(|_| BTreeSet::default()), seens: Arc::new(RwLock::new(HashMap::default())), event_map: Arc::new(RwLock::new(HashMap::default())), - tracking_flag: Arc::new(AtomicBool::new(false)), + tracking: Arc::new(AtomicBool::new(false)), + msg_relays_existed: Arc::new(AtomicBool::new(false)), signal_rx: rx, signal_tx: tx, tasks: smallvec![], @@ -175,7 +178,10 @@ impl ChatRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); - let status = self.tracking_flag.clone(); + + let tracking = self.tracking.clone(); + let msg_relays_existed = self.msg_relays_existed.clone(); + let seens = self.seens.clone(); let event_map = self.event_map.clone(); let trashes = self.trashes.downgrade(); @@ -199,10 +205,7 @@ impl ChatRegistry { }; match *message { - RelayMessage::Event { - event, - subscription_id, - } => { + RelayMessage::Event { event, .. } => { // Keep track of which relays have seen this event { let mut seens = seens.write().await; @@ -219,6 +222,9 @@ impl ChatRegistry { let current_user = signer.get_public_key().await?; if event.pubkey == current_user { + // Mark that the msg relays have been found + msg_relays_existed.store(true, Ordering::Release); + // Emit the inbox ready signal tx.send_async(Signal::inbox_ready(&event)).await?; } } @@ -234,8 +240,7 @@ impl ChatRegistry { // Map the rumor id to the gift wrap event id for later lookup { let mut event_map = event_map.write().await; - let dekey = subscription_id.as_ref() == &sub_id1; - event_map.insert(rumor.id.unwrap(), (event.id, dekey)); + event_map.insert(rumor.id.unwrap(), event.id); } // Check if the rumor has a recipient @@ -250,7 +255,7 @@ impl ChatRegistry { tx.send_async(signal).await?; } else { // Mark the chat still processing new messages - status.store(true, Ordering::Release); + tracking.store(true, Ordering::Release); } } Err(e) => { @@ -287,7 +292,7 @@ impl ChatRegistry { } Signal::InboxRelayNotFound => { this.update(cx, |_this, cx| { - cx.emit(ChatEvent::Error("Messaging Relays not found".into())); + cx.emit(ChatEvent::InboxRelayNotFound); })?; } Signal::Eose => { @@ -310,7 +315,7 @@ impl ChatRegistry { /// Tracking the status of unwrapping gift wrap events. fn tracking(&mut self, cx: &mut Context) { - let status = self.tracking_flag.clone(); + let status = self.tracking.clone(); let tx = self.signal_tx.clone(); self.tasks.push(cx.background_spawn(async move { @@ -328,8 +333,8 @@ impl ChatRegistry { })); } - /// Get contact list from relays - fn get_metadata(&mut self, cx: &mut Context) { + /// Get all necessary metadata from relays for current user + pub fn get_metadata(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); @@ -339,9 +344,7 @@ impl ChatRegistry { }; self.tasks.push(cx.background_spawn(async move { - let opts = SubscribeAutoCloseOptions::default() - .exit_policy(ReqExitPolicy::ExitOnEOSE) - .timeout(Some(Duration::from_secs(TIMEOUT))); + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); // Construct filter for msg relays let msg_relays = Filter::new() @@ -358,29 +361,27 @@ impl ChatRegistry { // Subscribe client .subscribe(vec![msg_relays, contact_list]) + .with_id(SubscriptionId::new("user-meta")) .close_on(opts) .await?; Ok(()) })); - let client = nostr.read(cx).client(); let tx = self.signal_tx.clone(); + let msg_relays_existed = self.msg_relays_existed.clone(); + // Reset the status flag + msg_relays_existed.store(false, Ordering::Release); + + // Wait for the msg relays to be found or timeout self.tasks.push(cx.background_spawn(async move { - loop { - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(public_key) - .limit(1); + // Wait for 5 seconds + smol::Timer::after(Duration::from_secs(5)).await; - if client.database().query(filter).await?.first().is_some() { - break; - } else { - tx.send_async(Signal::inbox_relay_not_found()).await?; - } - - smol::Timer::after(Duration::from_secs(5)).await; + // Then check if the msg relays have been found + if !msg_relays_existed.load(Ordering::Acquire) { + tx.send_async(Signal::inbox_relay_not_found()).await?; } Ok(()) @@ -439,7 +440,7 @@ impl ChatRegistry { /// Get the loading status of the chat registry pub fn loading(&self) -> bool { - self.tracking_flag.load(Ordering::Acquire) + self.tracking.load(Ordering::Acquire) } /// Get a weak reference to a room by its ID. @@ -491,7 +492,7 @@ impl ChatRegistry { self.event_map .read_blocking() .get(id) - .map(|(id, _dekey)| self.seen_on(id)) + .map(|id| self.seen_on(id)) } /// Get the relays that have seen a given gift wrap id. @@ -503,15 +504,6 @@ impl ChatRegistry { .unwrap_or_default() } - /// Check if a given rumor was encrypted by the dekey. - pub fn encrypted_by_dekey(&self, id: &EventId) -> bool { - self.event_map - .read_blocking() - .get(id) - .map(|(_, dekey)| *dekey) - .unwrap_or(false) - } - /// Add a new room to the start of list. pub fn add_room(&mut self, room: I, cx: &mut Context) where diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index c2d2a6d..43c07d4 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -470,12 +470,6 @@ impl ChatPanel { self.reports_by_id.read(cx).get(id).is_some() } - /// Check if a message was encrypted by the dekey - fn encrypted_by_dekey(&self, id: &EventId, cx: &App) -> bool { - let chat = ChatRegistry::global(cx); - chat.read(cx).encrypted_by_dekey(id) - } - /// Get all sent reports for a message by its ID fn sent_reports(&self, id: &EventId, cx: &App) -> Option> { self.reports_by_id.read(cx).get(id).cloned() @@ -848,7 +842,6 @@ impl ChatPanel { let replies = message.replies_to.as_slice(); let has_replies = !replies.is_empty(); let has_reports = self.has_reports(&id, cx); - let encrypted_by_dekey = self.encrypted_by_dekey(&id, cx); // Hide avatar setting let hide_avatar = AppSettings::get_hide_avatar(cx); @@ -894,17 +887,6 @@ impl ChatPanel { .text_color(cx.theme().text) .child(author.name()), ) - .when(encrypted_by_dekey, |this| { - this.child( - Button::new(format!("dekey-{ix}")) - .icon(IconName::Shield) - .ghost() - .xsmall() - .px_4() - .tooltip("Encrypted by Dekey") - .disabled(true), - ) - }) .child(message.created_at.to_human_time()) .when(has_reports, |this| { this.child(deferred(self.render_sent_reports(&id, cx))) diff --git a/crates/ui/src/notification.rs b/crates/ui/src/notification.rs index bf720be..4779a37 100644 --- a/crates/ui/src/notification.rs +++ b/crates/ui/src/notification.rs @@ -298,10 +298,10 @@ impl Render for Notification { let action = self.action_builder.clone().map(|builder| { builder(self, window, cx) - .xsmall() + .small() .primary() - .px_3() - .font_semibold() + .px_4() + .font_medium() }); let icon = match self.kind { @@ -364,8 +364,14 @@ impl Render for Notification { }) .when_some(content, |this, content| this.child(content)) .when_some(action, |this, action| { - this.gap_2() - .child(h_flex().w_full().flex_1().justify_end().child(action)) + this.gap_2().child( + h_flex() + .mt_2() + .w_full() + .flex_1() + .justify_end() + .child(action), + ) }), ) .child( diff --git a/desktop/src/workspace.rs b/desktop/src/workspace.rs index f487b1b..0a001f4 100644 --- a/desktop/src/workspace.rs +++ b/desktop/src/workspace.rs @@ -42,6 +42,7 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity { struct DeviceNotifcation; struct SignerNotifcation; struct RelayNotifcation; +struct MsgRelayNotification; #[derive(Action, Clone, PartialEq, Eq, Deserialize)] #[action(namespace = workspace, no_json)] @@ -184,6 +185,27 @@ impl Workspace { // Observe all events emitted by the chat registry cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| { match ev { + ChatEvent::InboxRelayNotFound => { + const MSG: &str = "Messaging Relays not found. Cannot receive messages."; + + window.push_notification( + Notification::warning(MSG) + .id::() + .autohide(false) + .action(|_this, _window, _cx| { + Button::new("retry").label("Retry").on_click( + move |_this, window, cx| { + let chat = ChatRegistry::global(cx); + chat.update(cx, |this, cx| { + this.get_metadata(cx); + }); + window.clear_notification::(cx); + }, + ) + }), + cx, + ); + } ChatEvent::OpenRoom(id) => { if let Some(room) = chat.read(cx).room(id, cx) { this.dock.update(cx, |this, cx| { -- 2.49.1 From 57a129fa93d3d5107e32e1e12b1dccfef989b5ca Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Fri, 5 Jun 2026 15:10:28 +0700 Subject: [PATCH 07/12] refactor nip4e --- crates/device/src/lib.rs | 120 +++++++++++++++++++-------------------- desktop/src/workspace.rs | 32 ++++++----- 2 files changed, 75 insertions(+), 77 deletions(-) diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 21cfd8f..d3dda01 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -2,6 +2,8 @@ use std::cell::Cell; use std::collections::HashSet; use std::path::PathBuf; use std::rc::Rc; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use anyhow::{Context as AnyhowContext, Error, anyhow}; @@ -13,7 +15,7 @@ use nostr_sdk::prelude::*; use person::PersonRegistry; use settings::AppSettings; use smallvec::{SmallVec, smallvec}; -use state::{Announcement, CLIENT_NAME, NostrRegistry, StateEvent, TIMEOUT}; +use state::{Announcement, CLIENT_NAME, NostrRegistry, StateEvent}; use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::Button; @@ -35,10 +37,10 @@ impl Global for GlobalDeviceRegistry {} pub enum DeviceEvent { /// A new encryption signer has been set Set, + /// User have not setup encryption key + NotSet, /// The device is requesting an encryption key Requesting, - /// The device is creating a new encryption key - Creating, /// An error occurred Error(SharedString), } @@ -57,12 +59,12 @@ impl DeviceEvent { /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md #[derive(Debug)] pub struct DeviceRegistry { - /// Whether the registry is currently initializing - pub initializing: bool, - /// Whether there is a pending request for encryption key approval pub pending_request: bool, + /// Whether an announcement has been made for this device + pub announcement_existed: Arc, + /// Async tasks tasks: Vec>>, @@ -114,8 +116,8 @@ impl DeviceRegistry { }); Self { - initializing: true, pending_request: false, + announcement_existed: Arc::new(AtomicBool::new(false)), tasks: vec![], _subscriptions: subscriptions, } @@ -124,9 +126,13 @@ impl DeviceRegistry { fn handle_notifications(&mut self, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let signer = nostr.read(cx).signer(); + + let announcement_existed = self.announcement_existed.clone(); let (tx, rx) = flume::bounded::(100); self.tasks.push(cx.background_spawn(async move { + let current_user = signer.get_public_key().await?; let mut notifications = client.notifications(); let mut processed_events = HashSet::new(); @@ -140,13 +146,19 @@ impl DeviceRegistry { } match event.kind { + Kind::Custom(10044) => { + if current_user == event.pubkey { + announcement_existed.store(true, Ordering::Relaxed); + tx.send_async(event.into_owned()).await?; + } + } Kind::Custom(4454) => { - if verify_author(&client, event.as_ref()).await { + if current_user == event.pubkey { tx.send_async(event.into_owned()).await?; } } Kind::Custom(4455) => { - if verify_author(&client, event.as_ref()).await { + if current_user == event.pubkey { tx.send_async(event.into_owned()).await?; } } @@ -161,6 +173,11 @@ impl DeviceRegistry { self.tasks.push(cx.spawn_in(window, async move |this, cx| { while let Ok(event) = rx.recv_async().await { match event.kind { + Kind::Custom(10044) => { + this.update_in(cx, |this, _window, cx| { + this.set_encryption(&event, cx); + })?; + } // New request event from other device Kind::Custom(4454) => { this.update_in(cx, |this, window, cx| { @@ -180,12 +197,6 @@ impl DeviceRegistry { })); } - /// Set whether the registry is currently initializing - fn set_initializing(&mut self, initializing: bool, cx: &mut Context) { - self.initializing = initializing; - cx.notify(); - } - /// Set whether there is a pending request for encryption key approval fn set_pending_request(&mut self, pending: bool, cx: &mut Context) { self.pending_request = pending; @@ -203,9 +214,8 @@ impl DeviceRegistry { self.tasks.push(cx.spawn(async move |this, cx| { signer.set_encryption_signer(new).await; - // Update state - this.update(cx, |this, cx| { - this.set_initializing(false, cx); + // Notify the UI via event + this.update(cx, |_this, cx| { cx.emit(DeviceEvent::Set); })?; @@ -232,13 +242,11 @@ impl DeviceRegistry { pub fn get_announcement(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let signer = nostr.read(cx).signer(); - // Show the loading state - self.set_initializing(true, cx); - - let task: Task> = cx.background_spawn(async move { - let signer = client.signer().context("Signer not found")?; + self.tasks.push(cx.background_spawn(async move { let public_key = signer.get_public_key().await?; + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); // Construct the filter for the device announcement event let filter = Filter::new() @@ -246,35 +254,35 @@ impl DeviceRegistry { .author(public_key) .limit(1); - // Stream events from user's write relays - let mut stream = client - .stream_events(filter) - .timeout(Duration::from_secs(TIMEOUT)) + client + .subscribe(filter) + .close_on(opts) + .with_id(SubscriptionId::new("nip4e")) .await?; - while let Some((_url, res)) = stream.next().await { - if let Ok(event) = res { - return Ok(event); - } - } + Ok(()) + })); - Err(anyhow!("Announcement not found")) - }); + let announcement_existed = self.announcement_existed.clone(); self.tasks.push(cx.spawn(async move |this, cx| { - match task.await { - Ok(event) => { - // Set encryption key from the announcement event - this.update(cx, |this, cx| { - this.set_encryption(&event, cx); - })?; - } - Err(_) => { - // User has no announcement, create a new one - this.update(cx, |this, cx| { - this.set_announcement(Keys::generate(), cx); - })?; - } + if !cx + .background_spawn(async move { + // Wait for 5 seconds + smol::Timer::after(Duration::from_secs(5)).await; + + // Then check if the msg relays have been found + if !announcement_existed.load(Ordering::Acquire) { + return true; + } + + false + }) + .await + { + this.update(cx, |_this, cx| { + cx.emit(DeviceEvent::NotSet); + })?; } Ok(()) @@ -285,9 +293,6 @@ impl DeviceRegistry { pub fn set_announcement(&mut self, keys: Keys, cx: &mut Context) { let task = self.create_encryption(keys, cx); - // Notify that we're creating a new encryption key - cx.emit(DeviceEvent::Creating); - self.tasks.push(cx.spawn(async move |this, cx| { match task.await { Ok(keys) => { @@ -446,10 +451,7 @@ impl DeviceRegistry { } Ok(None) => { this.update(cx, |this, cx| { - this.set_initializing(false, cx); this.wait_for_approval(cx); - - cx.emit(DeviceEvent::Requesting); })?; } Err(e) => { @@ -468,6 +470,8 @@ impl DeviceRegistry { let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); + cx.emit(DeviceEvent::Requesting); + self.tasks.push(cx.background_spawn(async move { let public_key = signer.get_public_key().await?; @@ -708,16 +712,6 @@ impl DeviceRegistry { struct DeviceNotification; -/// Verify the author of an event -async fn verify_author(client: &Client, event: &Event) -> bool { - if let Some(signer) = client.signer() - && let Ok(public_key) = signer.get_public_key().await - { - return public_key == event.pubkey; - } - false -} - /// Encrypt and store device keys in the local database. async fn set_keys(client: &Client, secret: &str) -> Result<(), Error> { let signer = client.signer().context("Signer not found")?; diff --git a/desktop/src/workspace.rs b/desktop/src/workspace.rs index 0a001f4..0ef6318 100644 --- a/desktop/src/workspace.rs +++ b/desktop/src/workspace.rs @@ -146,7 +146,7 @@ impl Workspace { match event { DeviceEvent::Requesting => { const MSG: &str = - "Coop has sent a request for an encryption key. Please open the other client then approve the request."; + "Please open other client and approve the request for encryption key."; let note = Notification::new() .id::() @@ -157,12 +157,25 @@ impl Workspace { window.push_notification(note, cx); } - DeviceEvent::Creating => { + DeviceEvent::NotSet => { + const MSG: &str = + "User're not setup encryption key yet. Do you want to create one?"; + let note = Notification::new() .id::() - .autohide(false) - .message("Creating encryption key") - .with_kind(NotificationKind::Info); + .message(MSG) + .with_kind(NotificationKind::Info) + .action(|_this, _window, _cx| { + Button::new("retry").label("Retry").on_click( + move |_this, window, cx| { + let device = DeviceRegistry::global(cx); + device.update(cx, |this, cx| { + this.set_announcement(Keys::generate(), cx); + }); + window.clear_notification::(cx); + }, + ) + }); window.push_notification(note, cx); } @@ -664,9 +677,6 @@ impl Workspace { let trash_messages = chat.read(cx).count_trash_messages(cx); let is_nip4e_enabled = AppSettings::get_encryption_key(cx); - let device = DeviceRegistry::global(cx); - let device_initializing = device.read(cx).initializing; - let nostr = NostrRegistry::global(cx); let signer = nostr.read(cx).signer(); @@ -720,12 +730,6 @@ impl Workspace { .tooltip("Decoupled encryption key") .small() .ghost() - .loading(device_initializing) - .when(device_initializing, |this| { - this.label("Dekey") - .xsmall() - .tooltip("Loading decoupled encryption key...") - }) .dropdown_menu(move |this, _window, _cx| { this.min_w(px(260.)) .label("Encryption Key") -- 2.49.1 From 04983be23f454ae9752b93c18af418400e05d3cd Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Sun, 7 Jun 2026 17:18:11 +0700 Subject: [PATCH 08/12] fix --- crates/chat_ui/src/lib.rs | 8 ++-- crates/device/src/lib.rs | 4 +- crates/settings/src/lib.rs | 83 ++++++++++++++++++-------------------- 3 files changed, 46 insertions(+), 49 deletions(-) diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 43c07d4..9fc609d 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -10,8 +10,8 @@ use gpui::{ AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ListAlignment, ListOffset, ListState, MouseButton, ObjectFit, ParentElement, PathPromptOptions, Render, SharedString, SharedUri, - StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, WeakEntity, Window, - deferred, div, img, list, px, red, relative, svg, white, + StatefulInteractiveElement, Styled, StyledImage, Subscription, Task, WeakEntity, Window, div, + img, list, px, red, relative, svg, white, }; use itertools::Itertools; use nostr_sdk::prelude::*; @@ -889,7 +889,7 @@ impl ChatPanel { ) .child(message.created_at.to_human_time()) .when(has_reports, |this| { - this.child(deferred(self.render_sent_reports(&id, cx))) + this.child(self.render_sent_reports(&id, cx)) }), ) .when(has_replies, |this| { @@ -1534,7 +1534,7 @@ impl Render for ChatPanel { this.upload(window, cx); })), ) - .child(Input::new(&self.input).appearance(false).text_sm().flex_1()) + .child(Input::new(&self.input).appearance(false).flex_1()) .child( h_flex() .pl_1() diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index d3dda01..dd69da1 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -89,14 +89,14 @@ impl DeviceRegistry { fn new(window: &mut Window, cx: &mut Context) -> Self { let nostr = NostrRegistry::global(cx); let settings = AppSettings::global(cx); - let is_nip4e_enabled = settings.read(cx).is_nip4e_enabled(); + let is_nip4e_enabled = settings.read(cx).is_nip4e_enabled(cx); let mut subscriptions = smallvec![]; subscriptions.push( // Subscribe to nostr state events cx.observe(&settings, move |this, settings, cx| { - if settings.read(cx).is_nip4e_enabled() { + if settings.read(cx).is_nip4e_enabled(cx) { this.get_announcement(cx); }; }), diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index e107994..9651e55 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -20,13 +20,15 @@ macro_rules! setting_accessors { $( paste::paste! { pub fn [](cx: &App) -> $type { - Self::global(cx).read(cx).values.$field.clone() + Self::global(cx).read(cx).inner.read(cx).$field.clone() } pub fn [](value: $type, cx: &mut App) { Self::global(cx).update(cx, |this, cx| { - this.values.$field = value; - cx.notify(); + this.inner.update(cx, |inner, cx| { + inner.$field = value; + cx.notify(); + }); }); } } @@ -185,7 +187,7 @@ impl Global for GlobalAppSettings {} /// Application settings pub struct AppSettings { /// Settings - values: Settings, + inner: Entity, /// Event subscriptions _subscriptions: SmallVec<[Subscription; 2]>, @@ -203,11 +205,12 @@ impl AppSettings { } fn new(window: &mut Window, cx: &mut Context) -> Self { + let inner = cx.new(|_| Settings::default()); let mut subscriptions = smallvec![]; subscriptions.push( // Observe and automatically save settings on changes - cx.observe_self(|this, cx| { + cx.observe(&inner, |this, _inner, cx| { this.save(cx); }), ); @@ -218,15 +221,17 @@ impl AppSettings { }); Self { - values: Settings::default(), + inner, _subscriptions: subscriptions, } } /// Update settings fn set_settings(&mut self, settings: Settings, cx: &mut Context) { - self.values = settings; - cx.notify(); + self.inner.update(cx, |this, cx| { + *this = settings; + cx.notify(); + }); } /// Load settings @@ -256,19 +261,16 @@ impl AppSettings { /// Save settings pub fn save(&mut self, cx: &mut Context) { - let settings = self.values.clone(); + let settings = self.inner.read(cx); - let task: Task> = cx.background_spawn(async move { - let path = config_dir().join(".settings"); - let content = serde_json::to_string(&settings)?; - - // Write settings to file - smol::fs::write(&path, content).await?; - - Ok(()) - }); - - task.detach(); + if let Ok(content) = serde_json::to_string(&settings) { + cx.background_spawn(async move { + let path = config_dir().join(".settings"); + // Write settings to file + smol::fs::write(&path, content).await.ok(); + }) + .detach(); + } } /// Set theme @@ -277,8 +279,10 @@ impl AppSettings { T: Into, { // Update settings - self.values.theme = Some(theme.into()); - cx.notify(); + self.inner.update(cx, |this, cx| { + this.theme = Some(theme.into()); + cx.notify(); + }); // Apply the new theme self.apply_theme(window, cx); @@ -286,16 +290,17 @@ impl AppSettings { /// Reset theme pub fn reset_theme(&mut self, window: &mut Window, cx: &mut Context) { - self.values.theme = None; - cx.notify(); - + self.inner.update(cx, |this, cx| { + this.theme = None; + cx.notify(); + }); self.apply_theme(window, cx); } /// Apply theme pub fn apply_theme(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(name) = self.values.theme.as_ref() { - let mode = self.values.theme_mode; + if let Some(name) = self.inner.read(cx).theme.as_ref() { + let mode = self.inner.read(cx).theme_mode; if let Ok(new_theme) = ThemeFamily::from_assets(name) { Theme::apply_theme(Rc::new(new_theme), Some(window), cx); @@ -309,30 +314,22 @@ impl AppSettings { } /// Check if decoupling encryption key is enabled - pub fn is_nip4e_enabled(&self) -> bool { - self.values.encryption_key + pub fn is_nip4e_enabled(&self, cx: &App) -> bool { + self.inner.read(cx).encryption_key } /// Check if the given relay is already authenticated - pub fn trusted_relay(&self, url: &RelayUrl, _cx: &App) -> bool { - self.values.trusted_relays.iter().any(|relay| { + pub fn trusted_relay(&self, url: &RelayUrl, cx: &App) -> bool { + self.inner.read(cx).trusted_relays.iter().any(|relay| { relay.as_str_without_trailing_slash() == url.as_str_without_trailing_slash() }) } /// Add a relay to the trusted list pub fn add_trusted_relay(&mut self, url: &RelayUrl, cx: &mut Context) { - self.values.trusted_relays.insert(url.clone()); - cx.notify(); - } - - /// Add a room configuration - pub fn add_room_config(&mut self, id: u64, config: RoomConfig, cx: &mut Context) { - self.values - .room_configs - .entry(id) - .and_modify(|this| *this = config) - .or_default(); - cx.notify(); + self.inner.update(cx, |this, cx| { + this.trusted_relays.insert(url.clone()); + cx.notify(); + }); } } -- 2.49.1 From 0230fcff230623c6c386d891b00e4f9638cbdc8d Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Wed, 10 Jun 2026 10:17:40 +0700 Subject: [PATCH 09/12] wip --- Cargo.lock | 543 ++++++++++++++++--------- Cargo.toml | 12 +- crates/chat/src/lib.rs | 101 +++-- crates/chat/src/message.rs | 4 +- crates/chat/src/room.rs | 63 +-- crates/chat_ui/src/lib.rs | 4 +- crates/common/src/event.rs | 4 +- crates/device/src/lib.rs | 176 ++++---- crates/person/src/lib.rs | 2 +- crates/relay_auth/src/lib.rs | 11 +- crates/state/src/lib.rs | 468 ++------------------- crates/state/src/nip4e.rs | 5 +- crates/state/src/signer.rs | 134 ------ desktop/src/dialogs/accounts.rs | 257 ------------ desktop/src/dialogs/connect.rs | 115 ------ desktop/src/dialogs/import.rs | 127 +----- desktop/src/dialogs/mod.rs | 2 - desktop/src/dialogs/screening.rs | 36 +- desktop/src/panels/contact_list.rs | 20 +- desktop/src/panels/greeter.rs | 3 +- desktop/src/panels/messaging_relays.rs | 23 +- desktop/src/panels/profile.rs | 11 +- desktop/src/panels/relay_list.rs | 20 +- desktop/src/sidebar/mod.rs | 17 +- desktop/src/workspace.rs | 92 ++--- rust-toolchain.toml | 2 +- 26 files changed, 718 insertions(+), 1534 deletions(-) delete mode 100644 crates/state/src/signer.rs delete mode 100644 desktop/src/dialogs/accounts.rs delete mode 100644 desktop/src/dialogs/connect.rs diff --git a/Cargo.lock b/Cargo.lock index 540d77e..03dfff4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,7 +112,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "generic-array", ] @@ -123,8 +123,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", - "cipher", - "cpufeatures", + "cipher 0.4.4", + "cpufeatures 0.2.17", "zeroize", ] @@ -581,13 +581,14 @@ dependencies = [ [[package]] name = "async-wsocket" -version = "0.13.1" -source = "git+https://github.com/shadowylab/async-wsocket?rev=0fed6c9c6aec7393ee0e9cf3933d76914ab427d3#0fed6c9c6aec7393ee0e9cf3933d76914ab427d3" +version = "0.14.0" +source = "git+https://github.com/shadowylab/async-wsocket?rev=07c0fda670d05d695929cab55f41761b90aef9a0#07c0fda670d05d695929cab55f41761b90aef9a0" dependencies = [ "futures", "futures-util", "js-sys", "tokio", + "tokio-happy-eyeballs", "tokio-rustls", "tokio-socks", "tokio-tungstenite", @@ -769,12 +770,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - [[package]] name = "bech32" version = "0.11.1" @@ -787,7 +782,7 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -858,9 +853,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ "serde_core", ] @@ -889,6 +884,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block-padding" version = "0.3.3" @@ -1020,7 +1024,7 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "polling", "rustix 1.1.4", "slab", @@ -1045,7 +1049,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ - "cipher", + "cipher 0.4.4", ] [[package]] @@ -1121,8 +1125,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" dependencies = [ "cfg-if", - "cipher", - "cpufeatures", + "cipher 0.4.4", + "cpufeatures 0.2.17", ] [[package]] @@ -1133,7 +1137,7 @@ checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" dependencies = [ "aead", "chacha20", - "cipher", + "cipher 0.4.4", "poly1305", "zeroize", ] @@ -1191,9 +1195,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -1209,11 +1213,22 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", - "inout", + "crypto-common 0.1.7", + "inout 0.1.4", "zeroize", ] +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "block-buffer 0.12.0", + "crypto-common 0.2.2", + "inout 0.2.2", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -1274,6 +1289,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "cocoa" version = "0.25.0" @@ -1296,7 +1317,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block", "cocoa-foundation 0.2.0", "core-foundation 0.10.0", @@ -1326,7 +1347,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block", "core-foundation 0.10.0", "core-graphics-types 0.2.0", @@ -1348,8 +1369,9 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ + "gpui_util", "indexmap", "rustc-hash 2.1.2", ] @@ -1606,7 +1628,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation 0.10.0", "core-graphics-types 0.2.0", "foreign-types", @@ -1619,7 +1641,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation 0.9.4", "core-graphics-types 0.1.3", "foreign-types", @@ -1643,7 +1665,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation 0.10.0", "libc", ] @@ -1654,7 +1676,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4416167a69126e617f8d0a214af0e3c1dbdeffcb100ddf72dcd1a1ac9893c146" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block", "cfg-if", "core-foundation 0.10.0", @@ -1702,7 +1724,7 @@ version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be17b688510d934ce13f48a2beba700e11583e281e0fda99c22bb256a14eda73" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "fontdb", "harfrust", "linebender_resource_handle", @@ -1710,7 +1732,7 @@ dependencies = [ "rangemap", "rustc-hash 2.1.2", "self_cell", - "skrifa", + "skrifa 0.40.0", "smol_str", "swash", "sys-locale", @@ -1729,6 +1751,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -1788,6 +1819,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "ctor" version = "1.0.7" @@ -1798,6 +1838,15 @@ dependencies = [ "linktime-proc-macro", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "data-encoding" version = "2.11.0" @@ -1842,7 +1891,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "proc-macro2", "quote", @@ -1877,11 +1926,22 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "dirs" version = "5.0.1" @@ -1936,7 +1996,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2 0.6.2", "libc", "objc2 0.6.4", @@ -2274,7 +2334,7 @@ version = "25.12.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "rustc_version", ] @@ -2674,6 +2734,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -2787,7 +2848,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "gpu-descriptor-types", "hashbrown 0.15.5", ] @@ -2798,20 +2859,20 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "accesskit", "anyhow", "async-channel 2.5.0", "async-task", "bindgen", - "bitflags 2.11.1", + "bitflags 2.13.0", "block", "cbindgen", "chrono", @@ -2834,6 +2895,7 @@ dependencies = [ "gpui_macros", "gpui_shared_string", "gpui_util", + "heapless 0.9.3", "http_client", "image", "inventory", @@ -2885,14 +2947,14 @@ dependencies = [ [[package]] name = "gpui_linux" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "accesskit", "accesskit_unix", "anyhow", "as-raw-xcb-connection", "ashpd", - "bitflags 2.11.1", + "bitflags 2.13.0", "bytemuck", "calloop", "calloop-wayland-source", @@ -2936,7 +2998,7 @@ dependencies = [ [[package]] name = "gpui_macos" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "accesskit", "accesskit_macos", @@ -2981,7 +3043,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2992,7 +3054,7 @@ dependencies = [ [[package]] name = "gpui_platform" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "console_error_panic_hook", "gpui", @@ -3005,7 +3067,7 @@ dependencies = [ [[package]] name = "gpui_shared_string" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "schemars", "serde", @@ -3015,7 +3077,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "anyhow", "gpui", @@ -3026,7 +3088,7 @@ dependencies = [ [[package]] name = "gpui_util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "anyhow", "log", @@ -3035,7 +3097,7 @@ dependencies = [ [[package]] name = "gpui_web" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "anyhow", "console_error_panic_hook", @@ -3059,7 +3121,7 @@ dependencies = [ [[package]] name = "gpui_wgpu" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "anyhow", "bytemuck", @@ -3088,7 +3150,7 @@ dependencies = [ [[package]] name = "gpui_windows" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "accesskit", "accesskit_windows", @@ -3155,10 +3217,10 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9da2e5ae821f6e96664977bf974d6d6a2d6682f9ccee23e62ec1d134246845f9" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "bytemuck", "core_maths", - "read-fonts", + "read-fonts 0.37.0", "smallvec", ] @@ -3250,7 +3312,7 @@ version = "0.20.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d4f449bab7320c56003d37732a917e18798e2f1709d80263face2b4f9436ddb" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "byteorder", "heed-traits", "heed-types", @@ -3311,7 +3373,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", ] [[package]] @@ -3320,7 +3382,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", ] [[package]] @@ -3334,9 +3405,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -3368,7 +3439,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "anyhow", "async-compression", @@ -3384,7 +3455,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sha2", + "sha2 0.10.9", "tempfile", "url", "util", @@ -3393,7 +3464,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3405,6 +3476,15 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.10.1" @@ -3641,9 +3721,9 @@ checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" [[package]] name = "imgref" -version = "1.12.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" +checksum = "89194689a993ab15268672e99e7b0e19da2da3268ac682e8f02d29d4d1434cd7" [[package]] name = "indexmap" @@ -3676,6 +3756,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "hybrid-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -3858,13 +3947,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.99" +version = "0.3.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -3952,9 +4040,9 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libfuzzer-sys" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" dependencies = [ "arbitrary", "cc", @@ -3982,10 +4070,10 @@ version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "libc", "plain", - "redox_syscall 0.8.0", + "redox_syscall 0.8.1", ] [[package]] @@ -4019,9 +4107,9 @@ dependencies = [ [[package]] name = "link-section" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "014e440054ce8170890229eeef5bcda955305e056ec713de40ed366944483f09" +checksum = "c2b1dd6fe32e55c0fc0ea9493aa57459ca3cf4ff3c857c7d0302290150da6e4f" [[package]] name = "linkify" @@ -4084,9 +4172,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.30" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" dependencies = [ "serde_core", "value-bag", @@ -4228,13 +4316,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "anyhow", "bindgen", @@ -4285,7 +4373,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7047791b5bc903b8cd963014b355f71dc9864a9a0b727057676c1dcae5cbc15" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block", "core-graphics-types 0.2.0", "foreign-types", @@ -4354,7 +4442,7 @@ source = "git+https://github.com/zed-industries/wgpu.git?rev=357a0c56e0070480ad9 dependencies = [ "arrayvec", "bit-set", - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if", "cfg_aliases", "codespan-reporting", @@ -4414,7 +4502,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if", "cfg_aliases", "libc", @@ -4426,7 +4514,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cfg-if", "cfg_aliases", "libc", @@ -4468,8 +4556,8 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" -version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr?rev=46d66396467e07c3dfb71e5102104ecb7e9c6b64#46d66396467e07c3dfb71e5102104ecb7e9c6b64" +version = "0.45.0-alpha.1" +source = "git+https://github.com/rust-nostr/nostr#02a59264ed8c6075753e398cf4eb81f489d9a5af" dependencies = [ "aes", "base64", @@ -4480,7 +4568,7 @@ dependencies = [ "chacha20", "chacha20poly1305", "faster-hex", - "rand 0.9.4", + "rand 0.10.1", "scrypt", "secp256k1", "serde", @@ -4492,8 +4580,8 @@ dependencies = [ [[package]] name = "nostr-blossom" -version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr?rev=46d66396467e07c3dfb71e5102104ecb7e9c6b64#46d66396467e07c3dfb71e5102104ecb7e9c6b64" +version = "0.45.0-alpha.1" +source = "git+https://github.com/rust-nostr/nostr#02a59264ed8c6075753e398cf4eb81f489d9a5af" dependencies = [ "base64", "nostr", @@ -4503,8 +4591,8 @@ dependencies = [ [[package]] name = "nostr-connect" -version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr?rev=46d66396467e07c3dfb71e5102104ecb7e9c6b64#46d66396467e07c3dfb71e5102104ecb7e9c6b64" +version = "0.45.0-alpha.1" +source = "git+https://github.com/rust-nostr/nostr#02a59264ed8c6075753e398cf4eb81f489d9a5af" dependencies = [ "async-utility", "futures-core", @@ -4516,8 +4604,8 @@ dependencies = [ [[package]] name = "nostr-database" -version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr?rev=46d66396467e07c3dfb71e5102104ecb7e9c6b64#46d66396467e07c3dfb71e5102104ecb7e9c6b64" +version = "0.45.0-alpha.1" +source = "git+https://github.com/rust-nostr/nostr#02a59264ed8c6075753e398cf4eb81f489d9a5af" dependencies = [ "btreecap", "flatbuffers", @@ -4526,16 +4614,16 @@ dependencies = [ [[package]] name = "nostr-gossip" -version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr?rev=46d66396467e07c3dfb71e5102104ecb7e9c6b64#46d66396467e07c3dfb71e5102104ecb7e9c6b64" +version = "0.45.0-alpha.1" +source = "git+https://github.com/rust-nostr/nostr#02a59264ed8c6075753e398cf4eb81f489d9a5af" dependencies = [ "nostr", ] [[package]] name = "nostr-gossip-memory" -version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr?rev=46d66396467e07c3dfb71e5102104ecb7e9c6b64#46d66396467e07c3dfb71e5102104ecb7e9c6b64" +version = "0.45.0-alpha.1" +source = "git+https://github.com/rust-nostr/nostr#02a59264ed8c6075753e398cf4eb81f489d9a5af" dependencies = [ "indexmap", "lru", @@ -4546,8 +4634,8 @@ dependencies = [ [[package]] name = "nostr-lmdb" -version = "0.44.0" -source = "git+https://github.com/rust-nostr/nostr?rev=46d66396467e07c3dfb71e5102104ecb7e9c6b64#46d66396467e07c3dfb71e5102104ecb7e9c6b64" +version = "0.45.0-alpha.1" +source = "git+https://github.com/rust-nostr/nostr#02a59264ed8c6075753e398cf4eb81f489d9a5af" dependencies = [ "async-utility", "flume 0.12.0", @@ -4560,8 +4648,8 @@ dependencies = [ [[package]] name = "nostr-sdk" -version = "0.44.1" -source = "git+https://github.com/rust-nostr/nostr?rev=46d66396467e07c3dfb71e5102104ecb7e9c6b64#46d66396467e07c3dfb71e5102104ecb7e9c6b64" +version = "0.45.0-alpha.1" +source = "git+https://github.com/rust-nostr/nostr#02a59264ed8c6075753e398cf4eb81f489d9a5af" dependencies = [ "async-utility", "async-wsocket", @@ -4760,7 +4848,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -4786,7 +4874,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4798,7 +4886,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "dispatch2", "objc2 0.6.4", ] @@ -4827,7 +4915,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -4839,7 +4927,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "objc2 0.6.4", "objc2-core-foundation", ] @@ -4850,7 +4938,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4862,7 +4950,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2 0.6.2", "objc2 0.6.4", "objc2-foundation 0.3.2", @@ -4874,7 +4962,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -4887,7 +4975,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "objc2 0.6.4", "objc2-core-foundation", "objc2-foundation 0.3.2", @@ -4952,21 +5040,21 @@ dependencies = [ "async-lock", "blocking", "cbc", - "cipher", - "digest", + "cipher 0.4.4", + "digest 0.10.7", "endi", "futures-lite 2.6.1", "futures-util", "getrandom 0.4.2", "hkdf", - "hmac", + "hmac 0.12.1", "md-5", "num", "num-bigint-dig", - "pbkdf2", + "pbkdf2 0.12.2", "serde", "serde_bytes", - "sha2", + "sha2 0.10.9", "subtle", "zbus", "zbus_macros", @@ -5071,17 +5159,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "paste" version = "1.0.15" @@ -5125,8 +5202,18 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest", - "hmac", + "digest 0.10.7", + "hmac 0.12.1", +] + +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest 0.11.3", + "hmac 0.13.0", ] [[package]] @@ -5138,7 +5225,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "collections", "serde", @@ -5340,7 +5427,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "crc32fast", "fdeflate", "flate2", @@ -5379,7 +5466,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", "opaque-debug", "universal-hash", ] @@ -5525,7 +5612,7 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "getopts", "memchr", "pulldown-cmark-escape", @@ -5684,6 +5771,16 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -5722,6 +5819,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "range-alloc" version = "0.1.5" @@ -5833,6 +5936,16 @@ dependencies = [ "font-types", ] +[[package]] +name = "read-fonts" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4ed38b89c2c77ff968c524145ad65fb010f38af5c7a224b53b81d47ac2daa81" +dependencies = [ + "bytemuck", + "font-types", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -5848,16 +5961,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] name = "redox_syscall" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c7591fa2c6b601dfcfe5f043f65a1c39fcdf50efefcd7f1572e538c1f4b398d" +checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -5905,7 +6018,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "derive_refineable", ] @@ -5972,6 +6085,7 @@ dependencies = [ "base64", "bytes", "futures-core", + "h2", "http", "http-body", "http-body-util", @@ -6004,7 +6118,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "anyhow", "bytes", @@ -6105,7 +6219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" dependencies = [ "globset", - "sha2", + "sha2 0.10.9", "walkdir", ] @@ -6152,7 +6266,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -6165,7 +6279,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys 0.12.1", @@ -6190,9 +6304,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -6270,7 +6384,7 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "bytemuck", "core_maths", "log", @@ -6290,11 +6404,12 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "salsa20" -version = "0.10.2" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c" dependencies = [ - "cipher", + "cfg-if", + "cipher 0.5.2", ] [[package]] @@ -6318,7 +6433,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "async-task", "backtrace", @@ -6393,14 +6508,14 @@ dependencies = [ [[package]] name = "scrypt" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +checksum = "d87af57419b594aa23fa95f09f0e06d80d84ba01c26148c43844cad6ff4485f0" dependencies = [ - "password-hash", - "pbkdf2", + "cfg-if", + "pbkdf2 0.13.0", "salsa20", - "sha2", + "sha2 0.11.0", ] [[package]] @@ -6434,7 +6549,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation 0.10.0", "core-foundation-sys", "libc", @@ -6619,8 +6734,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] @@ -6636,8 +6751,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -6724,7 +6850,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" dependencies = [ "bytemuck", - "read-fonts", + "read-fonts 0.37.0", +] + +[[package]] +name = "skrifa" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c34617370ae968efb7161bb2beb517d9084659aae19e24b89e3db25b46e4564" +dependencies = [ + "bytemuck", + "read-fonts 0.39.2", ] [[package]] @@ -6809,7 +6945,7 @@ version = "0.4.0+sdk-1.4.341.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -6941,7 +7077,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "heapless 0.9.3", "log", @@ -7046,11 +7182,11 @@ dependencies = [ [[package]] name = "swash" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "842f3cd369c2ba38966204f983eaa5e54a8e84a7d7159ed36ade2b6c335aae64" +checksum = "d1804632b66a35ca2b1d277eb0a138e10f46cb365b9a6d297e876b69ef79de43" dependencies = [ - "skrifa", + "skrifa 0.42.1", "yazi", "zeno", ] @@ -7124,7 +7260,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -7371,6 +7507,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tokio-happy-eyeballs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8564c32dfb6f4257f8bc6edfc178a34af97520e0b7b9815500c55eb3d092f29f" +dependencies = [ + "tokio", +] + [[package]] name = "tokio-macros" version = "2.7.0" @@ -7558,7 +7703,7 @@ version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "bytes", "futures-util", "http", @@ -7815,9 +7960,9 @@ checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" [[package]] name = "unicode-vo" @@ -7843,7 +7988,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "subtle", ] @@ -7926,7 +8071,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "anyhow", "async-fs", @@ -7965,7 +8110,7 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "perf", "quote", @@ -7974,9 +8119,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.23.2" +version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -8121,9 +8266,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.122" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" dependencies = [ "cfg-if", "once_cell", @@ -8134,9 +8279,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.72" +version = "0.4.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf" dependencies = [ "js-sys", "wasm-bindgen", @@ -8144,9 +8289,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.122" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -8154,9 +8299,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.122" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" dependencies = [ "bumpalo", "proc-macro2", @@ -8167,9 +8312,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.122" +version = "0.2.123" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" dependencies = [ "unicode-ident", ] @@ -8227,7 +8372,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "hashbrown 0.15.5", "indexmap", "semver", @@ -8253,7 +8398,7 @@ version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "rustix 1.1.4", "wayland-backend", "wayland-scanner", @@ -8276,7 +8421,7 @@ version = "0.32.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -8288,7 +8433,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -8301,7 +8446,7 @@ version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -8333,9 +8478,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.99" +version = "0.3.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69" dependencies = [ "js-sys", "wasm-bindgen", @@ -8415,7 +8560,7 @@ version = "29.0.3" source = "git+https://github.com/zed-industries/wgpu.git?rev=357a0c56e0070480ad9daea5d2eaa83150b79e88#357a0c56e0070480ad9daea5d2eaa83150b79e88" dependencies = [ "arrayvec", - "bitflags 2.11.1", + "bitflags 2.13.0", "bytemuck", "cfg-if", "cfg_aliases", @@ -8446,7 +8591,7 @@ dependencies = [ "arrayvec", "bit-set", "bit-vec", - "bitflags 2.11.1", + "bitflags 2.13.0", "bytemuck", "cfg_aliases", "document-features", @@ -8503,7 +8648,7 @@ dependencies = [ "arrayvec", "ash", "bit-set", - "bitflags 2.11.1", + "bitflags 2.13.0", "block2 0.6.2", "bytemuck", "cfg-if", @@ -8561,7 +8706,7 @@ name = "wgpu-types" version = "29.0.3" source = "git+https://github.com/zed-industries/wgpu.git?rev=357a0c56e0070480ad9daea5d2eaa83150b79e88#357a0c56e0070480ad9daea5d2eaa83150b79e88" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "bytemuck", "js-sys", "log", @@ -9323,7 +9468,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.11.1", + "bitflags 2.13.0", "indexmap", "log", "serde", @@ -9439,7 +9584,7 @@ name = "xim-parser" version = "0.2.1" source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -9497,9 +9642,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -9620,7 +9765,7 @@ name = "zed-font-kit" version = "0.14.1-zed" source = "git+https://github.com/zed-industries/font-kit?rev=94b0f28166665e8fd2f53ff6d268a14955c82269#94b0f28166665e8fd2f53ff6d268a14955c82269" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "byteorder", "core-foundation 0.10.0", "core-graphics 0.24.0", @@ -9825,7 +9970,7 @@ dependencies = [ [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "anyhow", "chrono", @@ -9842,7 +9987,7 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" dependencies = [ "tracing", "tracing-subscriber", @@ -9853,7 +9998,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#46ac5758a5562f05b786b88cf3600b9334abeb7d" +source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" [[package]] name = "zune-core" diff --git a/Cargo.toml b/Cargo.toml index b53f547..3865dce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,12 +19,12 @@ gpui_tokio = { git = "https://github.com/zed-industries/zed" } reqwest_client = { git = "https://github.com/zed-industries/zed" } # Nostr -nostr-lmdb = { git = "https://github.com/rust-nostr/nostr", rev = "46d66396467e07c3dfb71e5102104ecb7e9c6b64" } -nostr-connect = { git = "https://github.com/rust-nostr/nostr", rev = "46d66396467e07c3dfb71e5102104ecb7e9c6b64" } -nostr-blossom = { git = "https://github.com/rust-nostr/nostr", rev = "46d66396467e07c3dfb71e5102104ecb7e9c6b64" } -nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr", rev = "46d66396467e07c3dfb71e5102104ecb7e9c6b64" } -nostr-sdk = { git = "https://github.com/rust-nostr/nostr", rev = "46d66396467e07c3dfb71e5102104ecb7e9c6b64" } -nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip96", "nip59", "nip49", "nip44" ], rev = "46d66396467e07c3dfb71e5102104ecb7e9c6b64" } +nostr-lmdb = { git = "https://github.com/rust-nostr/nostr", } +nostr-connect = { git = "https://github.com/rust-nostr/nostr" } +nostr-blossom = { git = "https://github.com/rust-nostr/nostr" } +nostr-gossip-memory = { git = "https://github.com/rust-nostr/nostr" } +nostr-sdk = { git = "https://github.com/rust-nostr/nostr" } +nostr = { git = "https://github.com/rust-nostr/nostr", features = [ "nip59", "nip49", "nip44" ] } # Others anyhow = "1.0.44" diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index f5346e7..fb38881 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -16,7 +16,7 @@ use gpui::{ use nostr_sdk::prelude::*; use smallvec::{SmallVec, smallvec}; use smol::lock::RwLock; -use state::{CoopSigner, DEVICE_GIFTWRAP, NostrRegistry, StateEvent, USER_GIFTWRAP}; +use state::{DEVICE_GIFTWRAP, NostrRegistry, USER_GIFTWRAP}; mod message; mod room; @@ -137,15 +137,17 @@ impl ChatRegistry { /// Create a new chat registry instance fn new(window: &mut Window, cx: &mut Context) -> Self { let nostr = NostrRegistry::global(cx); + let user_signer = nostr.read(cx).signer.clone(); let (tx, rx) = flume::unbounded::(); let mut subscriptions = smallvec![]; subscriptions.push( // Subscribe to the signer event - cx.subscribe(&nostr, |this, _state, event, cx| { - if event == &StateEvent::SignerSet { + cx.observe(&user_signer, |this, signer, cx| { + if let Some(keys) = signer.read(cx).clone() { this.reset(cx); + this.handle_notifications(keys, cx); this.get_metadata(cx); this.get_rooms(cx); }; @@ -154,7 +156,6 @@ impl ChatRegistry { // Run at the end of the current cycle cx.defer_in(window, |this, _window, cx| { - this.handle_notifications(cx); this.tracking(cx); this.get_rooms(cx); }); @@ -174,10 +175,9 @@ impl ChatRegistry { } /// Handle nostr notifications - fn handle_notifications(&mut self, cx: &mut Context) { + fn handle_notifications(&mut self, signer: Keys, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); let tracking = self.tracking.clone(); let msg_relays_existed = self.msg_relays_existed.clone(); @@ -219,7 +219,7 @@ impl ChatRegistry { // Handle msg relays event to determine when the app is ready to subscribe if event.kind == Kind::InboxRelays { - let current_user = signer.get_public_key().await?; + let current_user = signer.get_public_key_async().await?; if event.pubkey == current_user { // Mark that the msg relays have been found @@ -265,10 +265,10 @@ impl ChatRegistry { } } } - RelayMessage::EndOfStoredEvents(id) => { - if id.as_ref() == &sub_id1 || id.as_ref() == &sub_id2 { - tx.send_async(Signal::eose()).await?; - } + RelayMessage::EndOfStoredEvents(id) + if (id.as_ref() == &sub_id1 || id.as_ref() == &sub_id2) => + { + tx.send_async(Signal::eose()).await?; } _ => {} } @@ -337,9 +337,8 @@ impl ChatRegistry { pub fn get_metadata(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); - let Some(public_key) = signer.public_key() else { + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { return; }; @@ -392,11 +391,14 @@ impl ChatRegistry { fn get_messages(&mut self, msg_relays: &Event, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); - let urls: Vec = nip17::extract_relay_list(msg_relays).cloned().collect(); + let urls: Vec = nip17::extract_relay_list(msg_relays).collect(); + + let Some(signer) = nostr.read(cx).signer(cx) else { + return; + }; let task: Task> = cx.background_spawn(async move { - let public_key = signer.get_public_key().await?; + let public_key = signer.get_public_key_async().await?; let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); let id = SubscriptionId::new(format!("{}-msg", public_key.to_hex())); @@ -510,11 +512,12 @@ impl ChatRegistry { I: Into + 'static, { let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); + + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; cx.spawn(async move |this, cx| { - let signer = client.signer()?; - let public_key = signer.get_public_key().await.ok()?; let room: Room = room.into().organize(&public_key); this.update(cx, |this, cx| { @@ -643,10 +646,11 @@ impl ChatRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - cx.background_spawn(async move { - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + return Task::ready(Err(anyhow!("Signer is required"))); + }; + cx.background_spawn(async move { // Get contacts let contacts = client .database() @@ -723,15 +727,15 @@ impl ChatRegistry { /// Updates room ordering based on the most recent messages. pub fn new_message(&mut self, message: NewMessage, cx: &mut Context) { let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); + + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; match self.rooms.iter().find(|e| e.read(cx).id == message.room) { Some(room) => { room.update(cx, |this, cx| { - if this.kind == RoomKind::Request - && let Some(public_key) = signer.public_key() - && message.rumor.pubkey == public_key - { + if this.kind == RoomKind::Request && message.rumor.pubkey == public_key { this.set_ongoing(cx); } this.push_message(message, cx); @@ -760,7 +764,7 @@ impl ChatRegistry { /// Unwraps a gift-wrapped event and processes its contents. async fn extract_rumor( client: &Client, - signer: &Arc, + signer: &Keys, gift_wrap: &Event, ) -> Result { // Try to get cached rumor first @@ -784,8 +788,9 @@ async fn extract_rumor( } /// Helper method to try unwrapping with different signers -async fn try_unwrap(signer: &Arc, gift_wrap: &Event) -> Result { - // Try with the device signer first +async fn try_unwrap(signer: &Keys, gift_wrap: &Event) -> Result { + /* + * // Try with the device signer first if let Some(signer) = signer.get_encryption_signer().await { log::info!("trying with encryption key"); if let Ok(unwrapped) = try_unwrap_with(gift_wrap, &signer).await { @@ -795,19 +800,17 @@ async fn try_unwrap(signer: &Arc, gift_wrap: &Event) -> Result(gift_wrap: &Event, signer: &T) -> Result -where - T: NostrSigner + 'static, -{ +async fn try_unwrap_with(gift_wrap: &Event, signer: &Keys) -> Result { // Get the sealed event let seal = signer - .nip44_decrypt(&gift_wrap.pubkey, &gift_wrap.content) + .nip44_decrypt_async(&gift_wrap.pubkey, &gift_wrap.content) .await?; // Verify the sealed event @@ -815,7 +818,10 @@ where seal.verify_with_ctx(&SECP256K1)?; // Get the rumor event - let rumor = signer.nip44_decrypt(&seal.pubkey, &seal.content).await?; + let rumor = signer + .nip44_decrypt_async(&seal.pubkey, &seal.content) + .await?; + let rumor = UnsignedEvent::from_json(rumor)?; Ok(UnwrappedGift { @@ -836,26 +842,17 @@ async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Resul tags.push(Tag::identifier(id)); // Add a reference to the rumor's author - tags.push(Tag::custom( - TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::A)), - [author], - )); + tags.push(Tag::custom("a", [author])); // Add a conversation id - tags.push(Tag::custom( - TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::C)), - [conversation.to_string()], - )); + tags.push(Tag::custom("c", [conversation.to_string()])); // Add a reference to the rumor's id tags.push(Tag::event(rumor_id)); // Add references to the rumor's participants - for receiver in rumor.tags.public_keys().copied() { - tags.push(Tag::custom( - TagKind::SingleLetter(SingleLetterTag::lowercase(Alphabet::P)), - [receiver], - )); + for receiver in rumor.tags.public_keys() { + tags.push(Tag::custom("P", [receiver])); } // Convert rumor to json @@ -864,7 +861,7 @@ async fn set_rumor(client: &Client, id: EventId, rumor: &UnsignedEvent) -> Resul // Construct the event let event = EventBuilder::new(Kind::ApplicationSpecificData, content) .tags(tags) - .sign(&Keys::generate()) + .finalize_async(&Keys::generate()) .await?; // Save the event to the database @@ -890,7 +887,7 @@ async fn get_rumor(client: &Client, gift_wrap: EventId) -> Result u64 { let mut hasher = DefaultHasher::new(); - let mut pubkeys: Vec = rumor.tags.public_keys().copied().collect(); + let mut pubkeys: Vec = rumor.tags.public_keys().collect(); pubkeys.push(rumor.pubkey); pubkeys.sort(); pubkeys.dedup(); diff --git a/crates/chat/src/message.rs b/crates/chat/src/message.rs index 3234226..10a8ba6 100644 --- a/crates/chat/src/message.rs +++ b/crates/chat/src/message.rs @@ -242,13 +242,13 @@ fn extract_mentions(content: &str) -> Vec { fn extract_reply_ids(inner: &Tags) -> Vec { let mut replies_to = vec![]; - for tag in inner.filter(TagKind::e()) { + for tag in inner.iter().filter(|tag| tag.kind() == "e") { if let Some(id) = tag.content().and_then(|id| EventId::parse(id).ok()) { replies_to.push(id); } } - for tag in inner.filter(TagKind::q()) { + for tag in inner.iter().filter(|tag| tag.kind() == "q") { if let Some(id) = tag.content().and_then(|id| EventId::parse(id).ok()) { replies_to.push(id); } diff --git a/crates/chat/src/room.rs b/crates/chat/src/room.rs index c77a1fb..50c5827 100644 --- a/crates/chat/src/room.rs +++ b/crates/chat/src/room.rs @@ -4,6 +4,7 @@ use std::time::Duration; use anyhow::{Error, anyhow}; use common::EventExt; +use device::DeviceRegistry; use gpui::{App, AppContext, Context, EventEmitter, SharedString, Task}; use itertools::Itertools; use nostr_sdk::prelude::*; @@ -21,7 +22,7 @@ pub struct SendReport { pub receiver: PublicKey, pub gift_wrap_id: Option, pub error: Option, - pub output: Option>, + pub output: Option>, } impl SendReport { @@ -41,7 +42,7 @@ impl SendReport { } /// Set the output. - pub fn output(mut self, output: Output) -> Self { + pub fn output(mut self, output: Output) -> Self { self.output = Some(output); self } @@ -171,7 +172,8 @@ impl From<&UnsignedEvent> for Room { let members = val.extract_public_keys(); let subject = val .tags - .find(TagKind::Subject) + .iter() + .find(|tag| tag.kind() == "subject") .and_then(|tag| tag.content().map(|s| s.to_owned().into())); Room { @@ -205,7 +207,7 @@ impl Room { // WARNING: never sign this event let mut event = EventBuilder::new(Kind::PrivateDirectMessage, "") .tags(tags) - .build(author); + .finalize_unsigned(author); // Ensure that the ID is set event.ensure_id(); @@ -425,7 +427,7 @@ impl Room { let nostr = NostrRegistry::global(cx); // Get current user's public key - let sender = nostr.read(cx).signer().public_key()?; + let sender = nostr.read(cx).signer_pubkey(cx)?; // Get all members, excluding the sender let members: Vec = self @@ -440,9 +442,7 @@ impl Room { // Add subject tag if present if let Some(value) = self.subject.as_ref() { - tags.push(Tag::from_standardized_without_cell(TagStandard::Subject( - value.to_string(), - ))); + tags.push(Tag::custom("subject", vec![value.to_string()])); } // Add all reply tags @@ -452,19 +452,20 @@ impl Room { // Add all receiver tags for member in members.into_iter() { - tags.push(Tag::from_standardized_without_cell( - TagStandard::PublicKey { + tags.push( + Nip01Tag::PublicKey { public_key: member.public_key(), - relay_url: member.messaging_relay_hint(), - alias: None, - uppercase: false, - }, - )); + relay_hint: member.messaging_relay_hint(), + } + .to_tag(), + ); } // Construct a direct message rumor event // WARNING: never sign and send this event to relays - let mut event = EventBuilder::new(kind, content).tags(tags).build(sender); + let mut event = EventBuilder::new(kind, content) + .tags(tags) + .finalize_unsigned(sender); // Ensure that the ID is set event.ensure_id(); @@ -475,13 +476,18 @@ impl Room { /// Send rumor event to all members's messaging relays pub fn send(&self, rumor: UnsignedEvent, cx: &App) -> Option>> { let config = self.config.clone(); - let persons = PersonRegistry::global(cx); + + let device = DeviceRegistry::global(cx); + let encryption_signer = device.read(cx).signer(cx); + let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); // Get current user's public key - let public_key = nostr.read(cx).signer().public_key()?; + let user_signer = nostr.read(cx).signer(cx)?; + let public_key = nostr.read(cx).signer_pubkey(cx)?; + + let persons = PersonRegistry::global(cx); let sender = persons.read(cx).get(&public_key, cx); // Get all members (excluding sender) @@ -496,9 +502,6 @@ impl Room { let signer_kind = config.signer_kind(); let backup = config.backup(); - let user_signer = signer.get().await; - let encryption_signer = signer.get_encryption_signer().await; - let mut sents = 0; let mut reports = Vec::new(); @@ -592,17 +595,14 @@ impl Room { } // Helper function to send a gift-wrapped event -async fn send_gift_wrap( +async fn send_gift_wrap( client: &Client, - signer: &T, + signer: &Keys, receiver: &Person, rumor: &UnsignedEvent, config: &SignerKind, -) -> Result -where - T: NostrSigner + 'static, -{ - let k_tag = Tag::custom(TagKind::k(), vec!["14"]); +) -> Result { + let k_tag = Tag::custom("k", vec!["14"]); let mut extra_tags = vec![k_tag]; // Determine the receiver public key based on the config @@ -627,7 +627,10 @@ where }; // Construct the gift wrap event - let event = EventBuilder::gift_wrap(signer, &receiver, rumor.clone(), extra_tags).await?; + let event = nip59::GiftWrapBuilder::new(receiver, rumor.clone()) + .extra_tags(extra_tags) + .finalize_async(signer) + .await?; // Send the gift wrap event and collect the report let report = client diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 9fc609d..6b87781 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -234,7 +234,7 @@ impl ChatPanel { match &*status { SendStatus::Ok { id, relay } => { if output.id() == id { - output.success.insert(relay.clone()); + output.success.insert(relay.clone(), EventSendStatus::Sent); } } SendStatus::Failed { id, relay, message } => { @@ -1158,7 +1158,7 @@ impl ChatPanel { .text_xs() .font_semibold() .line_height(relative(1.25)) - .child(SharedString::from(url.to_string())), + .child(SharedString::from(url.0.to_string())), ) .child( div() diff --git a/crates/common/src/event.rs b/crates/common/src/event.rs index 194cddf..e115775 100644 --- a/crates/common/src/event.rs +++ b/crates/common/src/event.rs @@ -18,7 +18,7 @@ impl EventExt for Event { } fn extract_public_keys(&self) -> Vec { - let mut public_keys: Vec = self.tags.public_keys().copied().collect(); + let mut public_keys: Vec = self.tags.public_keys().collect(); public_keys.push(self.pubkey); public_keys.into_iter().unique().collect() @@ -46,7 +46,7 @@ impl EventExt for UnsignedEvent { } fn extract_public_keys(&self) -> Vec { - let mut public_keys: Vec = self.tags.public_keys().copied().collect(); + let mut public_keys: Vec = self.tags.public_keys().collect(); public_keys.push(self.pubkey); public_keys.into_iter().unique().sorted().collect() } diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index dd69da1..1b1a462 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -65,6 +65,9 @@ pub struct DeviceRegistry { /// Whether an announcement has been made for this device pub announcement_existed: Arc, + /// Signer + signer: Entity>, + /// Async tasks tasks: Vec>>, @@ -87,7 +90,10 @@ impl DeviceRegistry { /// Create a new device registry instance fn new(window: &mut Window, cx: &mut Context) -> Self { + let signer = cx.new(|_| None); let nostr = NostrRegistry::global(cx); + let user_signer = nostr.read(cx).signer.clone(); + let settings = AppSettings::global(cx); let is_nip4e_enabled = settings.read(cx).is_nip4e_enabled(cx); @@ -104,8 +110,8 @@ impl DeviceRegistry { subscriptions.push( // Subscribe to nostr state events - cx.subscribe(&nostr, move |this, _e, event, cx| { - if event == &StateEvent::SignerSet && is_nip4e_enabled { + cx.observe(&user_signer, move |this, signer, cx| { + if signer.read(cx).is_some() && is_nip4e_enabled { this.get_announcement(cx); }; }), @@ -116,6 +122,7 @@ impl DeviceRegistry { }); Self { + signer, pending_request: false, announcement_existed: Arc::new(AtomicBool::new(false)), tasks: vec![], @@ -126,13 +133,15 @@ impl DeviceRegistry { fn handle_notifications(&mut self, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); let announcement_existed = self.announcement_existed.clone(); let (tx, rx) = flume::bounded::(100); + let Some(current_user) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; + self.tasks.push(cx.background_spawn(async move { - let current_user = signer.get_public_key().await?; let mut notifications = client.notifications(); let mut processed_events = HashSet::new(); @@ -203,24 +212,18 @@ impl DeviceRegistry { cx.notify(); } + /// Get the signer + pub fn signer(&self, cx: &App) -> Option { + self.signer.read(cx).clone() + } + /// Set the decoupled encryption key for the current user - fn set_signer(&mut self, new: S, cx: &mut Context) - where - S: NostrSigner + 'static, - { - let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); - - self.tasks.push(cx.spawn(async move |this, cx| { - signer.set_encryption_signer(new).await; - - // Notify the UI via event - this.update(cx, |_this, cx| { - cx.emit(DeviceEvent::Set); - })?; - - Ok(()) - })); + fn set_signer(&mut self, new: Keys, cx: &mut Context) { + self.signer.update(cx, |this, cx| { + *this = Some(new); + cx.notify(); + }); + cx.emit(DeviceEvent::Set); } /// Backup the encryption's secret key to a file @@ -228,8 +231,12 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let Some(signer) = nostr.read(cx).signer(cx) else { + return Task::ready(Err(anyhow!("Signer is required"))); + }; + cx.background_spawn(async move { - let keys = get_keys(&client).await?; + let keys = get_keys(&client, &signer).await?; let content = keys.secret_key().to_bech32()?; smol::fs::write(path, &content).await?; @@ -242,16 +249,18 @@ impl DeviceRegistry { pub fn get_announcement(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); + + let Some(current_user) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; self.tasks.push(cx.background_spawn(async move { - let public_key = signer.get_public_key().await?; let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); // Construct the filter for the device announcement event let filter = Filter::new() .kind(Kind::Custom(10044)) - .author(public_key) + .author(current_user) .limit(1); client @@ -319,15 +328,19 @@ impl DeviceRegistry { let secret = keys.secret_key().to_secret_hex(); let n = keys.public_key(); + let Some(signer) = nostr.read(cx).signer(cx) else { + return Task::ready(Err(anyhow!("Signer is required"))); + }; + cx.background_spawn(async move { // Construct an announcement event - let builder = EventBuilder::new(Kind::Custom(10044), "").tags(vec![ - Tag::custom(TagKind::custom("n"), vec![n]), - Tag::client(CLIENT_NAME), - ]); - - // Sign the event with user's signer - let event = client.sign_event_builder(builder).await?; + let event = EventBuilder::new(Kind::Custom(10044), "") + .tags(vec![ + Tag::custom("n", vec![n]), + Tag::custom("client", vec![CLIENT_NAME]), + ]) + .finalize_async(&signer) + .await?; // Publish announcement client @@ -337,7 +350,7 @@ impl DeviceRegistry { .await?; // Save device keys to the database - set_keys(&client, &secret).await?; + set_keys(&client, &signer, &secret).await?; Ok(keys) }) @@ -348,12 +361,16 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let Some(signer) = nostr.read(cx).signer(cx) else { + return; + }; + let announcement = Announcement::from(event); let device_pubkey = announcement.public_key(); // Get encryption key from the database and compare with the announcement let task: Task> = cx.background_spawn(async move { - let keys = get_keys(&client).await?; + let keys = get_keys(&client, &signer).await?; // Compare the public key from the announcement with the one from the database if keys.public_key() != device_pubkey { @@ -382,10 +399,13 @@ impl DeviceRegistry { fn wait_for_request(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); + + let Some(signer) = nostr.read(cx).signer(cx) else { + return; + }; self.tasks.push(cx.background_spawn(async move { - let public_key = signer.get_public_key().await?; + let public_key = signer.get_public_key_async().await?; let id = SubscriptionId::new("dekey-requests"); // Construct a filter for encryption key requests @@ -405,13 +425,16 @@ impl DeviceRegistry { pub fn request(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); - let app_keys = nostr.read(cx).keys(); + let app_keys = Keys::generate(); let app_pubkey = app_keys.public_key(); + let Some(signer) = nostr.read(cx).signer(cx) else { + return; + }; + let task: Task, Error>> = cx.background_spawn(async move { - let public_key = signer.get_public_key().await?; + let public_key = signer.get_public_key_async().await?; // Construct a filter to get the latest approval event let filter = Filter::new() @@ -426,13 +449,13 @@ impl DeviceRegistry { // No approval event found, construct a request event None => { // Construct an event for device key request - let builder = EventBuilder::new(Kind::Custom(4454), "").tags(vec![ - Tag::custom(TagKind::custom("P"), vec![app_pubkey]), - Tag::client(CLIENT_NAME), - ]); - - // Sign the event with user's signer - let event = client.sign_event_builder(builder).await?; + let event = EventBuilder::new(Kind::Custom(4454), "") + .tags(vec![ + Tag::custom("P", vec![app_pubkey]), + Tag::custom("client", vec![CLIENT_NAME]), + ]) + .finalize_async(&signer) + .await?; // Send the event to write relays client.send_event(&event).to_nip65().await?; @@ -468,12 +491,15 @@ impl DeviceRegistry { fn wait_for_approval(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); + + let Some(signer) = nostr.read(cx).signer(cx) else { + return; + }; cx.emit(DeviceEvent::Requesting); self.tasks.push(cx.background_spawn(async move { - let public_key = signer.get_public_key().await?; + let public_key = signer.get_public_key_async().await?; // Construct a filter for device key requests let filter = Filter::new() @@ -491,18 +517,19 @@ impl DeviceRegistry { /// Parse the approval event to get encryption key then set it fn extract_encryption(&mut self, event: Event, cx: &mut Context) { let nostr = NostrRegistry::global(cx); - let app_keys = nostr.read(cx).keys(); + let app_keys = Keys::generate(); let task: Task> = cx.background_spawn(async move { let master = event .tags - .find(TagKind::custom("P")) + .iter() + .find(|tag| tag.kind() == "P") .and_then(|tag| tag.content()) .and_then(|content| PublicKey::parse(content).ok()) .context("Invalid event's tags")?; let payload = event.content.as_str(); - let decrypted = app_keys.nip44_decrypt(&master, payload).await?; + let decrypted = app_keys.nip44_decrypt_async(&master, payload).await?; let secret = SecretKey::from_hex(&decrypted)?; let keys = Keys::new(secret); @@ -532,37 +559,42 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let Some(signer) = nostr.read(cx).signer(cx) else { + return; + }; + // Get user's write relays let event = event.clone(); let id: SharedString = event.id.to_hex().into(); let task: Task> = cx.background_spawn(async move { // Get device keys - let keys = get_keys(&client).await?; + let keys = get_keys(&client, &signer).await?; let secret = keys.secret_key().to_secret_hex(); // Extract the target public key from the event tags let target = event .tags - .find(TagKind::custom("P")) + .iter() + .find(|tag| tag.kind() == "P") .and_then(|tag| tag.content()) .and_then(|content| PublicKey::parse(content).ok()) .context("Target is not a valid public key")?; // Encrypt the device keys with the user's signer - let payload = keys.nip44_encrypt(&target, &secret).await?; + let payload = keys.nip44_encrypt_async(&target, &secret).await?; // Construct the response event // // P tag: the current device's public key // p tag: the requester's public key - let builder = EventBuilder::new(Kind::Custom(4455), payload).tags(vec![ - Tag::custom(TagKind::custom("P"), vec![keys.public_key().to_hex()]), - Tag::public_key(target), - ]); - - // Sign the builder - let event = client.sign_event_builder(builder).await?; + let event = EventBuilder::new(Kind::Custom(4455), payload) + .tags(vec![ + Tag::custom("P", vec![keys.public_key().to_hex()]), + Tag::public_key(target), + ]) + .finalize_async(&signer) + .await?; // Send the response event to the user's relay list client.send_event(&event).to_nip65().await?; @@ -713,18 +745,14 @@ impl DeviceRegistry { struct DeviceNotification; /// Encrypt and store device keys in the local database. -async fn set_keys(client: &Client, secret: &str) -> Result<(), Error> { - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; - - // Encrypt the value - let content = signer.nip44_encrypt(&public_key, secret).await?; +async fn set_keys(client: &Client, signer: &Keys, secret: &str) -> Result<(), Error> { + let public_key = signer.get_public_key_async().await?; + let content = signer.nip44_encrypt_async(&public_key, secret).await?; // Construct the application data event let event = EventBuilder::new(Kind::ApplicationSpecificData, content) .tag(Tag::identifier(IDENTIFIER)) - .build(public_key) - .sign(&Keys::generate()) + .finalize_async(signer) .await?; // Save the event to the database @@ -734,9 +762,8 @@ async fn set_keys(client: &Client, secret: &str) -> Result<(), Error> { } /// Get device keys from the local database. -async fn get_keys(client: &Client) -> Result { - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; +async fn get_keys(client: &Client, signer: &Keys) -> Result { + let public_key = signer.get_public_key_async().await?; let filter = Filter::new() .kind(Kind::ApplicationSpecificData) @@ -744,7 +771,10 @@ async fn get_keys(client: &Client) -> Result { .author(public_key); if let Some(event) = client.database().query(filter).await?.first() { - let content = signer.nip44_decrypt(&public_key, &event.content).await?; + let content = signer + .nip44_decrypt_async(&public_key, &event.content) + .await?; + let secret = SecretKey::parse(&content)?; let keys = Keys::new(secret); diff --git a/crates/person/src/lib.rs b/crates/person/src/lib.rs index 3f72f99..b8423f1 100644 --- a/crates/person/src/lib.rs +++ b/crates/person/src/lib.rs @@ -242,7 +242,7 @@ impl PersonRegistry { /// Set messaging relays for a person fn set_messaging_relays(&mut self, event: &Event, cx: &mut App) { - let urls: Vec = nip17::extract_relay_list(event).cloned().collect(); + let urls: Vec = nip17::extract_relay_list(event).collect(); if let Some(person) = self.persons.get(&event.pubkey) { person.update(cx, |person, cx| { diff --git a/crates/relay_auth/src/lib.rs b/crates/relay_auth/src/lib.rs index dc63cc7..13ed5ca 100644 --- a/crates/relay_auth/src/lib.rs +++ b/crates/relay_auth/src/lib.rs @@ -193,15 +193,20 @@ impl RelayAuth { fn auth(&self, req: &Arc, cx: &App) -> Task> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let req = req.clone(); + + let Some(signer) = nostr.read(cx).signer(cx) else { + return Task::ready(Err(anyhow!("Signer is required"))); + }; // Get all pending events for the relay + let req = req.clone(); let pending_events = self.get_pending_events(req.url(), cx); cx.background_spawn(async move { // Construct event - let builder = EventBuilder::auth(req.challenge(), req.url().clone()); - let event = client.sign_event_builder(builder).await?; + let event = EventBuilder::auth(req.challenge(), req.url().clone()) + .finalize_async(&signer) + .await?; // Get the event ID let id = event.id; diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 2fc4c51..0546279 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -1,9 +1,7 @@ use std::collections::HashMap; -use std::path::PathBuf; -use std::sync::Arc; use std::time::Duration; -use anyhow::{Context as AnyhowContext, Error, anyhow}; +use anyhow::{Error, anyhow}; use common::config_dir; use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Task, Window}; use nostr_connect::prelude::*; @@ -15,13 +13,11 @@ mod blossom; mod constants; mod nip05; mod nip4e; -mod signer; pub use blossom::*; pub use constants::*; pub use nip4e::*; pub use nip05::*; -pub use signer::*; pub fn init(window: &mut Window, cx: &mut App) { // rustls uses the `aws_lc_rs` provider by default @@ -48,12 +44,6 @@ pub enum StateEvent { Connecting, /// Connected to the bootstrapping relay Connected, - /// Creating the signer - Creating, - /// Show the identity dialog - Show, - /// A new signer has been set - SignerSet, /// An error occurred Error(SharedString), } @@ -73,19 +63,8 @@ pub struct NostrRegistry { /// Nostr client client: Client, - /// Nostr signer - signer: Arc, - - /// All local stored identities - npubs: Entity>, - - /// Keys directory - key_dir: PathBuf, - - /// Master app keys used for various operations. - /// - /// Example: Nostr Connect and NIP-4e operations - app_keys: Keys, + /// Currently active signer + pub signer: Entity>, /// Tasks for asynchronous operations tasks: Vec>>, @@ -106,20 +85,7 @@ impl NostrRegistry { /// Create a new nostr instance fn new(window: &mut Window, cx: &mut Context) -> Self { - let key_dir = config_dir().join("keys"); - let app_keys = get_or_init_app_keys(cx).unwrap_or(Keys::generate()); - - // Construct the nostr signer - let signer = Arc::new(CoopSigner::new(app_keys.clone())); - - // Get all local stored npubs - let npubs = cx.new(|_| match Self::discover(&key_dir) { - Ok(npubs) => npubs, - Err(e) => { - log::error!("Failed to discover npubs: {e}"); - vec![] - } - }); + let signer = cx.new(|_| None); // Construct the nostr lmdb instance let lmdb = cx.foreground_executor().block_on(async move { @@ -130,7 +96,6 @@ impl NostrRegistry { // Construct the nostr client let client = ClientBuilder::default() - .signer(signer.clone()) .database(lmdb) .gossip(NostrGossipMemory::unbounded()) .gossip_config( @@ -139,7 +104,6 @@ impl NostrRegistry { .sync_idle_timeout(Duration::from_millis(100)) .no_background_refresh(), ) - .automatic_authentication(false) .connect_timeout(Duration::from_secs(10)) .sleep_when_idle(SleepWhenIdle::Enabled { timeout: Duration::from_secs(600), @@ -149,22 +113,11 @@ impl NostrRegistry { // Run at the end of current cycle cx.defer_in(window, |this, _window, cx| { this.connect(cx); - - if this.npubs.read(cx).is_empty() { - // Create an identity if none exists - this.create_identity(cx); - } else { - // Show the account selector dialog - cx.emit(StateEvent::Show); - } }); Self { client, signer, - npubs, - key_dir, - app_keys, tasks: vec![], } } @@ -174,46 +127,22 @@ impl NostrRegistry { self.client.clone() } - /// Get the nostr signer - pub fn signer(&self) -> Arc { - self.signer.clone() + /// Get the signer + pub fn signer(&self, cx: &App) -> Option { + self.signer.read(cx).clone() } - /// Get the npubs entity - pub fn npubs(&self) -> Entity> { - self.npubs.clone() + /// Get the public key of the signer + pub fn signer_pubkey(&self, cx: &App) -> Option { + self.signer.read(cx).as_ref().map(|s| s.public_key()) } - /// Get the app keys - pub fn keys(&self) -> Keys { - self.app_keys.clone() - } - - /// Discover all npubs in the keys directory - fn discover(dir: &PathBuf) -> Result, Error> { - // Ensure keys directory exists - std::fs::create_dir_all(dir)?; - - let files = std::fs::read_dir(dir)?; - let mut entries = Vec::new(); - let mut npubs: Vec = Vec::new(); - - for file in files.flatten() { - let metadata = file.metadata()?; - let modified_time = metadata.modified()?; - let name = file.file_name().into_string().unwrap().replace(".npub", ""); - entries.push((modified_time, name)); - } - - // Sort by modification time (most recent first) - entries.sort_by(|a, b| b.0.cmp(&a.0)); - - for (_, name) in entries { - let public_key = PublicKey::parse(&name)?; - npubs.push(public_key); - } - - Ok(npubs) + /// Set the signer to the given keys + pub fn set_signer(&mut self, new_keys: Keys, cx: &mut Context) { + self.signer.update(cx, |this, cx| { + *this = Some(new_keys); + cx.notify(); + }); } /// Connect to the bootstrapping relays @@ -258,278 +187,8 @@ impl NostrRegistry { })); } - /// Get the secret for a given npub. - pub fn get_secret( - &self, - public_key: PublicKey, - cx: &App, - ) -> Task, Error>> { - let npub = public_key.to_bech32().unwrap(); - let key_path = self.key_dir.join(format!("{}.npub", npub)); - let app_keys = self.app_keys.clone(); - - if let Ok(payload) = std::fs::read_to_string(key_path) - && !payload.is_empty() - { - return cx.background_spawn(async move { - let decrypted = app_keys.nip44_decrypt(&public_key, &payload).await?; - let secret = SecretKey::parse(&decrypted)?; - let keys = Keys::new(secret); - - Ok(keys.into_nostr_signer()) - }); - } - - Task::ready(Err(anyhow::anyhow!("No secret found"))) - } - - /// Add a new npub to the keys directory - fn write_secret( - &self, - public_key: PublicKey, - secret: String, - cx: &App, - ) -> Task> { - let npub = public_key.to_bech32().unwrap(); - let key_path = self.key_dir.join(format!("{}.npub", npub)); - let app_keys = self.app_keys.clone(); - - cx.background_spawn(async move { - // If the secret starts with "bunker://" (nostr connect), use it directly; otherwise, encrypt it - let content = if secret.starts_with("bunker://") { - secret - } else { - app_keys.nip44_encrypt(&public_key, &secret).await? - }; - - // Write the encrypted secret to the keys directory - smol::fs::write(key_path, &content).await?; - - Ok(()) - }) - } - - /// Remove a secret - pub fn remove_secret(&mut self, public_key: &PublicKey, cx: &mut Context) { - let public_key = public_key.to_owned(); - let npub = public_key.to_bech32().unwrap(); - - let keys_dir = config_dir().join("keys"); - let key_path = keys_dir.join(format!("{}.npub", npub)); - - // Remove the secret file from the keys directory - std::fs::remove_file(key_path).ok(); - - self.npubs.update(cx, |this, cx| { - this.retain(|k| k != &public_key); - cx.notify(); - }); - } - - /// Create a new identity - pub fn create_identity(&mut self, cx: &mut Context) { - let client = self.client(); - let keys = Keys::generate(); - let async_keys = keys.clone(); - - // Emit creating event - cx.emit(StateEvent::Creating); - - // Create the write secret task - let write_secret = - self.write_secret(keys.public_key(), keys.secret_key().to_secret_hex(), cx); - - // Run async tasks in background - let task: Task> = cx.background_spawn(async move { - let signer = async_keys.into_nostr_signer(); - - // Construct relay list event - let relay_list = default_relay_list(); - let event = EventBuilder::relay_list(relay_list).sign(&signer).await?; - - // Publish relay list - client - .send_event(&event) - .to(BOOTSTRAP_RELAYS) - .ack_policy(AckPolicy::none()) - .await?; - - // Construct the default metadata - let name = petname::petname(2, "-").unwrap_or("Cooper".to_string()); - let avatar = Url::parse(&format!("https://avatar.vercel.sh/{name}")).unwrap(); - let metadata = Metadata::new().display_name(&name).picture(avatar); - let event = EventBuilder::metadata(&metadata).sign(&signer).await?; - - // Publish metadata event - client - .send_event(&event) - .to_nip65() - .ack_policy(AckPolicy::none()) - .await?; - - // Construct the default contact list - let contacts = vec![Contact::new(PublicKey::parse(COOP_PUBKEY).unwrap())]; - let event = EventBuilder::contact_list(contacts).sign(&signer).await?; - - // Publish contact list event - client - .send_event(&event) - .to_nip65() - .ack_policy(AckPolicy::none()) - .await?; - - // Construct the default messaging relay list - let relays = default_messaging_relays(); - let event = EventBuilder::nip17_relay_list(relays).sign(&signer).await?; - - // Publish messaging relay list event - client.send_event(&event).to_nip65().await?; - - // Write user's credentials to the system keyring - write_secret.await?; - - Ok(()) - }); - - self.tasks.push(cx.spawn(async move |this, cx| { - match task.await { - Ok(_) => { - this.update(cx, |this, cx| { - this.set_signer(keys, cx); - })?; - } - Err(e) => { - this.update(cx, |_this, cx| { - cx.emit(StateEvent::error(e.to_string())); - })?; - } - }; - - Ok(()) - })); - } - - /// Set the signer for the nostr client and verify the public key - pub fn set_signer(&mut self, new: T, cx: &mut Context) - where - T: NostrSigner + 'static, - { - let client = self.client(); - let signer = self.signer(); - - // Create a task to update the signer and verify the public key - let task: Task> = cx.background_spawn(async move { - // Update signer and unsubscribe - signer.switch(new).await; - client.unsubscribe_all().await?; - - // Verify and get public key - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; - - log::info!("Signer's public key: {}", public_key); - Ok(public_key) - }); - - self.tasks.push(cx.spawn(async move |this, cx| { - match task.await { - Ok(public_key) => { - this.update(cx, |this, cx| { - // Add public key to npubs if not already present - this.npubs.update(cx, |this, cx| { - if !this.contains(&public_key) { - this.push(public_key); - cx.notify(); - } - }); - - // Emit signer changed event - cx.emit(StateEvent::SignerSet); - })?; - } - Err(e) => { - this.update(cx, |_this, cx| { - cx.emit(StateEvent::error(e.to_string())); - })?; - } - }; - - Ok(()) - })); - } - - /// Add a key signer to keyring - pub fn add_key_signer(&mut self, keys: &Keys, cx: &mut Context) { - let keys = keys.clone(); - let write_secret = - self.write_secret(keys.public_key(), keys.secret_key().to_secret_hex(), cx); - - self.tasks.push(cx.spawn(async move |this, cx| { - match write_secret.await { - Ok(_) => { - this.update(cx, |this, cx| { - this.set_signer(keys, cx); - })?; - } - Err(e) => { - this.update(cx, |_this, cx| { - cx.emit(StateEvent::error(e.to_string())); - })?; - } - }; - - Ok(()) - })); - } - - /// Add a nostr connect signer to keyring - pub fn add_nip46_signer(&mut self, nip46: &NostrConnect, cx: &mut Context) { - let nip46 = nip46.clone(); - let async_nip46 = nip46.clone(); - - // Connect and verify the remote signer - let task: Task> = - cx.background_spawn(async move { - let uri = async_nip46.bunker_uri().await?; - let public_key = async_nip46.get_public_key().await?; - - Ok((public_key, uri)) - }); - - self.tasks.push(cx.spawn(async move |this, cx| { - match task.await { - Ok((public_key, uri)) => { - // Create the write secret task - let write_secret = this.read_with(cx, |this, cx| { - this.write_secret(public_key, uri.to_string(), cx) - })?; - - match write_secret.await { - Ok(_) => { - this.update(cx, |this, cx| { - this.set_signer(nip46, cx); - })?; - } - Err(e) => { - this.update(cx, |_this, cx| { - cx.emit(StateEvent::error(e.to_string())); - })?; - } - } - } - Err(e) => { - this.update(cx, |_this, cx| { - cx.emit(StateEvent::error(e.to_string())); - })?; - } - }; - - Ok(()) - })); - } - /// Get the public key of a NIP-05 address - pub fn get_address(&self, addr: Nip05Address, cx: &App) -> Task> { + pub fn query_address(&self, addr: Nip05Address, cx: &App) -> Task> { let client = self.client(); let http_client = cx.http_client(); @@ -566,7 +225,7 @@ impl NostrRegistry { // Get the address task if the query is a valid NIP-05 address let address_task = if let Ok(addr) = Nip05Address::parse(&query) { - Some(self.get_address(addr, cx)) + Some(self.query_address(addr, cx)) } else { None }; @@ -638,13 +297,19 @@ impl NostrRegistry { let client = self.client(); let query = query.to_string(); + let Some(signer) = self.signer.read(cx).clone() else { + return Task::ready(Err(anyhow!("Signer is required"))); + }; + cx.background_spawn(async move { // Construct a vertex request event - let builder = EventBuilder::new(Kind::Custom(5315), "").tags(vec![ - Tag::custom(TagKind::custom("param"), vec!["search", &query]), - Tag::custom(TagKind::custom("param"), vec!["limit", "10"]), - ]); - let event = client.sign_event_builder(builder).await?; + let event = EventBuilder::new(Kind::Custom(5315), "") + .tags(vec![ + Tag::custom("param", vec!["search", &query]), + Tag::custom("param", vec!["limit", "10"]), + ]) + .finalize_async(&signer) + .await?; // Send the event to vertex relays let output = client.send_event(&event).to(WOT_RELAYS).await?; @@ -694,78 +359,3 @@ impl NostrRegistry { }) } } - -/// Get or create new app keys -fn get_or_init_app_keys(cx: &App) -> Result { - let read = cx.read_credentials(CLIENT_NAME); - let stored_keys: Option = cx.foreground_executor().block_on(async move { - if let Ok(Some((_, secret))) = read.await { - SecretKey::from_slice(&secret).map(Keys::new).ok() - } else { - None - } - }); - - if let Some(keys) = stored_keys { - Ok(keys) - } else { - let keys = Keys::generate(); - let user = keys.public_key().to_hex(); - let secret = keys.secret_key().to_secret_bytes(); - let write = cx.write_credentials(CLIENT_NAME, &user, &secret); - - cx.foreground_executor().block_on(async move { - if let Err(e) = write.await { - log::error!("Keyring not available or panic: {e}") - } - }); - - Ok(keys) - } -} - -fn default_relay_list() -> Vec<(RelayUrl, Option)> { - vec![ - ( - RelayUrl::parse("wss://relay.nostr.net").unwrap(), - Some(RelayMetadata::Write), - ), - ( - RelayUrl::parse("wss://relay.primal.net").unwrap(), - Some(RelayMetadata::Write), - ), - ( - RelayUrl::parse("wss://relay.damus.io").unwrap(), - Some(RelayMetadata::Read), - ), - ( - RelayUrl::parse("wss://nos.lol").unwrap(), - Some(RelayMetadata::Read), - ), - ( - RelayUrl::parse("wss://nostr.superfriends.online").unwrap(), - None, - ), - ] -} - -fn default_messaging_relays() -> Vec { - vec![ - RelayUrl::parse("wss://nos.lol").unwrap(), - RelayUrl::parse("wss://nip17.com").unwrap(), - RelayUrl::parse("wss://auth.nostr1.com").unwrap(), - ] -} - -#[derive(Debug, Clone)] -pub struct CoopAuthUrlHandler; - -impl AuthUrlHandler for CoopAuthUrlHandler { - #[allow(mismatched_lifetime_syntaxes)] - fn on_auth_url(&self, auth_url: Url) -> BoxedFuture> { - Box::pin(async move { - webbrowser::open(auth_url.as_str())?; - Ok(()) - }) - } -} diff --git a/crates/state/src/nip4e.rs b/crates/state/src/nip4e.rs index 4aae264..475795c 100644 --- a/crates/state/src/nip4e.rs +++ b/crates/state/src/nip4e.rs @@ -16,14 +16,15 @@ impl From<&Event> for Announcement { let public_key = val .tags .iter() - .find(|tag| tag.kind().as_str() == "n") + .find(|tag| tag.kind() == "n") .and_then(|tag| tag.content()) .and_then(|c| PublicKey::parse(c).ok()) .unwrap_or(val.pubkey); let client_name = val .tags - .find(TagKind::Client) + .iter() + .find(|tag| tag.kind() == "client") .and_then(|tag| tag.content()) .map(|c| c.to_string()); diff --git a/crates/state/src/signer.rs b/crates/state/src/signer.rs deleted file mode 100644 index c6e9b20..0000000 --- a/crates/state/src/signer.rs +++ /dev/null @@ -1,134 +0,0 @@ -use std::borrow::Cow; -use std::result::Result; -use std::sync::Arc; - -use nostr_sdk::prelude::*; -use smol::lock::RwLock; - -#[derive(Debug)] -pub struct CoopSigner { - /// User's signer - signer: RwLock>, - - /// User's signer public key - signer_pkey: RwLock>, - - /// Specific signer for encryption purposes - encryption_signer: RwLock>>, -} - -impl CoopSigner { - pub fn new(signer: T) -> Self - where - T: IntoNostrSigner, - { - Self { - signer: RwLock::new(signer.into_nostr_signer()), - signer_pkey: RwLock::new(None), - encryption_signer: RwLock::new(None), - } - } - - /// Get the current signer. - pub async fn get(&self) -> Arc { - self.signer.read().await.clone() - } - - /// Get the encryption signer. - pub async fn get_encryption_signer(&self) -> Option> { - self.encryption_signer.read().await.clone() - } - - /// Get public key - /// - /// Ensure to call this method after the signer has been initialized. - /// Otherwise, it will panic. - pub fn public_key(&self) -> Option { - *self.signer_pkey.read_blocking() - } - - /// Switch the current signer to a new signer. - pub async fn switch(&self, new: T) - where - T: IntoNostrSigner, - { - let new_signer = new.into_nostr_signer(); - let public_key = new_signer.get_public_key().await.ok(); - let mut signer = self.signer.write().await; - let mut signer_pkey = self.signer_pkey.write().await; - let mut encryption_signer = self.encryption_signer.write().await; - - // Switch to the new signer - *signer = new_signer; - - // Update the public key - *signer_pkey = public_key; - - // Reset the encryption signer - *encryption_signer = None; - } - - /// Set the encryption signer. - pub async fn set_encryption_signer(&self, new: T) - where - T: IntoNostrSigner, - { - let mut encryption_signer = self.encryption_signer.write().await; - *encryption_signer = Some(new.into_nostr_signer()); - } -} - -impl NostrSigner for CoopSigner { - #[allow(mismatched_lifetime_syntaxes)] - fn backend(&self) -> SignerBackend { - SignerBackend::Custom(Cow::Borrowed("custom")) - } - - fn get_public_key<'a>(&'a self) -> BoxedFuture<'a, Result> { - Box::pin(async move { self.get().await.get_public_key().await }) - } - - fn sign_event<'a>( - &'a self, - unsigned: UnsignedEvent, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { self.get().await.sign_event(unsigned).await }) - } - - fn nip04_encrypt<'a>( - &'a self, - public_key: &'a PublicKey, - content: &'a str, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { self.get().await.nip04_encrypt(public_key, content).await }) - } - - fn nip04_decrypt<'a>( - &'a self, - public_key: &'a PublicKey, - encrypted_content: &'a str, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { - self.get() - .await - .nip04_decrypt(public_key, encrypted_content) - .await - }) - } - - fn nip44_encrypt<'a>( - &'a self, - public_key: &'a PublicKey, - content: &'a str, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { self.get().await.nip44_encrypt(public_key, content).await }) - } - - fn nip44_decrypt<'a>( - &'a self, - public_key: &'a PublicKey, - payload: &'a str, - ) -> BoxedFuture<'a, Result> { - Box::pin(async move { self.get().await.nip44_decrypt(public_key, payload).await }) - } -} diff --git a/desktop/src/dialogs/accounts.rs b/desktop/src/dialogs/accounts.rs deleted file mode 100644 index c780046..0000000 --- a/desktop/src/dialogs/accounts.rs +++ /dev/null @@ -1,257 +0,0 @@ -use anyhow::Error; -use gpui::prelude::FluentBuilder; -use gpui::{ - App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, - SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window, div, px, -}; -use nostr_sdk::prelude::*; -use person::PersonRegistry; -use state::{NostrRegistry, StateEvent}; -use theme::ActiveTheme; -use ui::avatar::Avatar; -use ui::button::{Button, ButtonVariants}; -use ui::indicator::Indicator; -use ui::{Disableable, Icon, IconName, Sizable, WindowExtension, h_flex, v_flex}; - -use crate::dialogs::connect::ConnectSigner; -use crate::dialogs::import::ImportKey; - -pub fn init(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| AccountSelector::new(window, cx)) -} - -/// Account selector -pub struct AccountSelector { - /// Public key currently being chosen for login - logging_in: Entity>, - - /// The error message displayed when an error occurs. - error: Entity>, - - /// Async tasks - tasks: Vec>>, - - /// Subscription to the signer events - _subscription: Option, -} - -impl AccountSelector { - pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let logging_in = cx.new(|_| None); - let error = cx.new(|_| None); - - // Subscribe to the signer events - let nostr = NostrRegistry::global(cx); - let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, window, cx| { - match event { - StateEvent::SignerSet => { - window.close_all_modals(cx); - window.refresh(); - } - StateEvent::Error(e) => { - this.set_error(e.to_string(), cx); - } - _ => {} - }; - }); - - Self { - logging_in, - error, - tasks: vec![], - _subscription: Some(subscription), - } - } - - fn logging_in(&self, public_key: &PublicKey, cx: &App) -> bool { - self.logging_in.read(cx) == &Some(*public_key) - } - - fn set_logging_in(&mut self, public_key: PublicKey, cx: &mut Context) { - self.logging_in.update(cx, |this, cx| { - *this = Some(public_key); - cx.notify(); - }); - } - - fn set_error(&mut self, error: T, cx: &mut Context) - where - T: Into, - { - self.error.update(cx, |this, cx| { - *this = Some(error.into()); - cx.notify(); - }); - - self.logging_in.update(cx, |this, cx| { - *this = None; - cx.notify(); - }) - } - - fn login(&mut self, public_key: PublicKey, window: &mut Window, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - let task = nostr.read(cx).get_secret(public_key, cx); - - // Mark the public key as being logged in - self.set_logging_in(public_key, cx); - - self.tasks.push(cx.spawn_in(window, async move |this, cx| { - match task.await { - Ok(signer) => { - nostr.update(cx, |this, cx| { - this.set_signer(signer, cx); - }); - } - Err(e) => { - this.update(cx, |this, cx| { - this.set_error(e.to_string(), cx); - })?; - } - }; - Ok(()) - })); - } - - fn remove(&mut self, public_key: PublicKey, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - - nostr.update(cx, |this, cx| { - this.remove_secret(&public_key, cx); - }); - } - - fn open_import(&mut self, window: &mut Window, cx: &mut Context) { - let import = cx.new(|cx| ImportKey::new(window, cx)); - - window.open_modal(cx, move |this, _window, _cx| { - this.width(px(460.)) - .title("Import a Secret Key or Bunker Connection") - .show_close(true) - .pb_2() - .child(import.clone()) - }); - } - - fn open_connect(&mut self, window: &mut Window, cx: &mut Context) { - let connect = cx.new(|cx| ConnectSigner::new(window, cx)); - - window.open_modal(cx, move |this, _window, _cx| { - this.width(px(460.)) - .title("Scan QR Code to Connect") - .show_close(true) - .pb_2() - .child(connect.clone()) - }); - } -} - -impl Render for AccountSelector { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let persons = PersonRegistry::global(cx); - let nostr = NostrRegistry::global(cx); - let npubs = nostr.read(cx).npubs(); - let loading = self.logging_in.read(cx).is_some(); - - v_flex() - .size_full() - .gap_2() - .when_some(self.error.read(cx).as_ref(), |this, error| { - this.child( - div() - .italic() - .text_xs() - .text_center() - .text_color(cx.theme().text_danger) - .child(error.clone()), - ) - }) - .children({ - let mut items = vec![]; - - for (ix, public_key) in npubs.read(cx).iter().enumerate() { - let profile = persons.read(cx).get(public_key, cx); - let logging_in = self.logging_in(public_key, cx); - - items.push( - h_flex() - .id(ix) - .group("") - .px_2() - .h_10() - .justify_between() - .w_full() - .rounded(cx.theme().radius) - .bg(cx.theme().ghost_element_background) - .hover(|this| this.bg(cx.theme().ghost_element_hover)) - .child( - h_flex() - .gap_2() - .child(Avatar::new(profile.avatar()).small()) - .child(div().text_sm().child(profile.name())), - ) - .when(logging_in, |this| this.child(Indicator::new().small())) - .when(!logging_in, |this| { - this.child( - h_flex() - .gap_1() - .invisible() - .group_hover("", |this| this.visible()) - .child( - Button::new(format!("del-{ix}")) - .icon(IconName::Close) - .ghost() - .small() - .disabled(logging_in) - .on_click(cx.listener({ - let public_key = *public_key; - move |this, _ev, _window, cx| { - cx.stop_propagation(); - this.remove(public_key, cx); - } - })), - ), - ) - }) - .when(!logging_in, |this| { - let public_key = *public_key; - this.on_click(cx.listener(move |this, _ev, window, cx| { - this.login(public_key, window, cx); - })) - }), - ); - } - - items - }) - .child(div().w_full().h_px().bg(cx.theme().border_variant)) - .child( - h_flex() - .gap_1() - .justify_end() - .w_full() - .child( - Button::new("input") - .icon(Icon::new(IconName::Usb)) - .label("Import") - .ghost() - .small() - .disabled(loading) - .on_click(cx.listener(move |this, _ev, window, cx| { - this.open_import(window, cx); - })), - ) - .child( - Button::new("qr") - .icon(Icon::new(IconName::Scan)) - .label("Scan QR to connect") - .ghost() - .small() - .disabled(loading) - .on_click(cx.listener(move |this, _ev, window, cx| { - this.open_connect(window, cx); - })), - ), - ) - } -} diff --git a/desktop/src/dialogs/connect.rs b/desktop/src/dialogs/connect.rs deleted file mode 100644 index 537cdc1..0000000 --- a/desktop/src/dialogs/connect.rs +++ /dev/null @@ -1,115 +0,0 @@ -use std::sync::Arc; -use std::time::Duration; - -use common::StringExt; -use gpui::prelude::FluentBuilder; -use gpui::{ - AppContext, Context, Entity, Image, IntoElement, ParentElement, Render, SharedString, Styled, - Subscription, Window, div, img, px, -}; -use nostr_connect::prelude::*; -use state::{ - CLIENT_NAME, CoopAuthUrlHandler, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT, NostrRegistry, - StateEvent, -}; -use theme::ActiveTheme; -use ui::v_flex; - -pub struct ConnectSigner { - /// QR Code - qr_code: Option>, - - /// Error message - error: Entity>, - - /// Subscription to the signer event - _subscription: Option, -} - -impl ConnectSigner { - pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let error = cx.new(|_| None); - - let nostr = NostrRegistry::global(cx); - let app_keys = nostr.read(cx).keys(); - - let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT); - let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap(); - - // Generate the nostr connect uri - let uri = NostrConnectUri::client(app_keys.public_key(), vec![relay], CLIENT_NAME); - - // Generate the nostr connect - let mut signer = NostrConnect::new(uri.clone(), app_keys.clone(), timeout, None).unwrap(); - - // Handle the auth request - signer.auth_url_handler(CoopAuthUrlHandler); - - // Generate a QR code for quick connection - let qr_code = uri.to_string().to_qr(); - - // Set signer in the background - nostr.update(cx, |this, cx| { - this.add_nip46_signer(&signer, cx); - }); - - // Subscribe to the signer event - let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| { - if let StateEvent::Error(e) = event { - this.set_error(e, cx); - } - }); - - Self { - qr_code, - error, - _subscription: Some(subscription), - } - } - - fn set_error(&mut self, message: S, cx: &mut Context) - where - S: Into, - { - self.error.update(cx, |this, cx| { - *this = Some(message.into()); - cx.notify(); - }); - } -} - -impl Render for ConnectSigner { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - const MSG: &str = "Scan with any Nostr Connect-compatible app to connect"; - - v_flex() - .size_full() - .items_center() - .justify_center() - .p_4() - .when_some(self.qr_code.as_ref(), |this, qr| { - this.child( - img(qr.clone()) - .size(px(256.)) - .rounded(cx.theme().radius_lg) - .border_1() - .border_color(cx.theme().border), - ) - }) - .when_some(self.error.read(cx).as_ref(), |this, error| { - this.child( - div() - .text_xs() - .text_center() - .text_color(cx.theme().text_danger) - .child(error.clone()), - ) - }) - .child( - div() - .text_xs() - .text_color(cx.theme().text_muted) - .child(SharedString::from(MSG)), - ) - } -} diff --git a/desktop/src/dialogs/import.rs b/desktop/src/dialogs/import.rs index 7810a53..ad2347b 100644 --- a/desktop/src/dialogs/import.rs +++ b/desktop/src/dialogs/import.rs @@ -7,15 +7,14 @@ use gpui::{ Subscription, Task, Window, div, }; use nostr_connect::prelude::*; -use smallvec::{SmallVec, smallvec}; -use state::{CoopAuthUrlHandler, NostrRegistry, StateEvent}; +use state::NostrRegistry; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::input::{Input, InputEvent, InputState}; use ui::{Disableable, v_flex}; #[derive(Debug)] -pub struct ImportKey { +pub struct ImportIdentity { /// Secret key input key_input: Entity, @@ -25,73 +24,43 @@ pub struct ImportKey { /// Error message error: Entity>, - /// Countdown timer for nostr connect - countdown: Entity>, - /// Whether the user is currently loading loading: bool, /// Async tasks tasks: Vec>>, - /// Event subscriptions - _subscriptions: SmallVec<[Subscription; 2]>, + /// Input subscription + _subscription: Option, } -impl ImportKey { +impl ImportIdentity { pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let nostr = NostrRegistry::global(cx); let key_input = cx.new(|cx| InputState::new(window, cx).masked(true)); let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true)); let error = cx.new(|_| None); - let countdown = cx.new(|_| None); - let mut subscriptions = smallvec![]; - - subscriptions.push( - // Subscribe to key input events and process login when the user presses enter + let input_subscription = cx.subscribe_in(&key_input, window, |this, _input, event, window, cx| { if let InputEvent::PressEnter { .. } = event { this.login(window, cx); }; - }), - ); - - subscriptions.push( - // Subscribe to the nostr signer event - cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| { - if let StateEvent::Error(e) = event { - this.set_error(e, cx); - } - }), - ); + }); Self { key_input, pass_input, error, - countdown, loading: false, tasks: vec![], - _subscriptions: subscriptions, + _subscription: Some(input_subscription), } } fn login(&mut self, window: &mut Window, cx: &mut Context) { - if self.loading { - return; - }; - // Prevent duplicate login requests - self.set_loading(true, cx); - let value = self.key_input.read(cx).value(); let password = self.pass_input.read(cx).value(); - if value.starts_with("bunker://") { - self.bunker(&value, window, cx); - return; - } - if value.starts_with("ncryptsec1") { self.ncryptsec(value, password, window, cx); return; @@ -103,52 +72,13 @@ impl ImportKey { // Update the signer nostr.update(cx, |this, cx| { - this.add_key_signer(&keys, cx); + this.set_signer(keys, cx); }); } else { self.set_error("Invalid key", cx); } } - fn bunker(&mut self, content: &str, window: &mut Window, cx: &mut Context) { - let Ok(uri) = NostrConnectUri::parse(content) else { - self.set_error("Bunker is not valid", cx); - return; - }; - - let nostr = NostrRegistry::global(cx); - let app_keys = nostr.read(cx).keys(); - let timeout = Duration::from_secs(30); - - // Construct the nostr connect signer - let mut signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap(); - - // Handle auth url with the default browser - signer.auth_url_handler(CoopAuthUrlHandler); - - // Set signer in the background - nostr.update(cx, |this, cx| { - this.add_nip46_signer(&signer, cx); - }); - - // Start countdown - self.tasks.push(cx.spawn_in(window, async move |this, cx| { - for i in (0..=30).rev() { - if i == 0 { - this.update(cx, |this, cx| { - this.set_countdown(None, cx); - })?; - } else { - this.update(cx, |this, cx| { - this.set_countdown(Some(i), cx); - })?; - } - cx.background_executor().timer(Duration::from_secs(1)).await; - } - Ok(()) - })); - } - fn ncryptsec(&mut self, content: S, pwd: S, window: &mut Window, cx: &mut Context) where S: Into, @@ -180,7 +110,7 @@ impl ImportKey { match task.await { Ok(keys) => { nostr.update(cx, |this, cx| { - this.add_key_signer(&keys, cx); + this.set_signer(keys, cx); }); } Err(e) => { @@ -198,12 +128,6 @@ impl ImportKey { where S: Into, { - // Reset the log in state - self.set_loading(false, cx); - - // Reset the countdown - self.set_countdown(None, cx); - // Update error message self.error.update(cx, |this, cx| { *this = Some(message.into()); @@ -224,22 +148,12 @@ impl ImportKey { Ok(()) })); } - - fn set_loading(&mut self, status: bool, cx: &mut Context) { - self.loading = status; - cx.notify(); - } - - fn set_countdown(&mut self, i: Option, cx: &mut Context) { - self.countdown.update(cx, |this, cx| { - *this = i; - cx.notify(); - }); - } } -impl Render for ImportKey { +impl Render for ImportIdentity { fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context) -> impl IntoElement { + const MSG: &str = "Coop isn't stored your identity secret in local device. Everything will be reset on the next login."; + v_flex() .size_full() .gap_2() @@ -249,7 +163,7 @@ impl Render for ImportKey { .gap_1() .text_sm() .text_color(cx.theme().text_muted) - .child("nsec or bunker://") + .child("nsec or ncryptsec://") .child(Input::new(&self.key_input)), ) .when( @@ -265,6 +179,7 @@ impl Render for ImportKey { ) }, ) + .child(div().text_xs().text_color(cx.theme().text_muted).child(MSG)) .child( Button::new("login") .label("Continue") @@ -275,18 +190,6 @@ impl Render for ImportKey { this.login(window, cx); })), ) - .when_some(self.countdown.read(cx).as_ref(), |this, i| { - this.child( - div() - .text_xs() - .text_center() - .text_color(cx.theme().text_muted) - .child(SharedString::from(format!( - "Approve connection request from your signer in {} seconds", - i - ))), - ) - }) .when_some(self.error.read(cx).as_ref(), |this, error| { this.child( div() diff --git a/desktop/src/dialogs/mod.rs b/desktop/src/dialogs/mod.rs index 831a2de..ffdf97b 100644 --- a/desktop/src/dialogs/mod.rs +++ b/desktop/src/dialogs/mod.rs @@ -1,5 +1,3 @@ -pub mod accounts; -pub mod connect; pub mod import; pub mod restore; pub mod screening; diff --git a/desktop/src/dialogs/screening.rs b/desktop/src/dialogs/screening.rs index 3a13e7a..3f5b375 100644 --- a/desktop/src/dialogs/screening.rs +++ b/desktop/src/dialogs/screening.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::time::Duration; -use anyhow::{Context as AnyhowContext, Error}; +use anyhow::Error; use common::TimestampExt; use gpui::prelude::FluentBuilder; use gpui::{ @@ -78,12 +78,13 @@ impl Screening { let client = nostr.read(cx).client(); let public_key = self.public_key; - let task: Task> = cx.background_spawn(async move { - let signer = client.signer().context("Signer not found")?; - let signer_pubkey = signer.get_public_key().await?; + let Some(current_user) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; + let task: Task> = cx.background_spawn(async move { // Check if user is in contact list - let contacts = client.database().contacts_public_keys(signer_pubkey).await; + let contacts = client.database().contacts_public_keys(current_user).await; let followed = contacts.unwrap_or_default().contains(&public_key); Ok(followed) @@ -105,16 +106,17 @@ impl Screening { let client = nostr.read(cx).client(); let public_key = self.public_key; - let task: Task, Error>> = cx.background_spawn(async move { - let signer = client.signer().context("Signer not found")?; - let signer_pubkey = signer.get_public_key().await?; + let Some(current_user) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; + let task: Task, Error>> = cx.background_spawn(async move { // Check mutual contacts let filter = Filter::new().kind(Kind::ContactList).pubkey(public_key); let mut mutual_contacts = vec![]; if let Ok(events) = client.database().query(filter).await { - for event in events.into_iter().filter(|ev| ev.pubkey != signer_pubkey) { + for event in events.into_iter().filter(|ev| ev.pubkey != current_user) { mutual_contacts.push(event.pubkey); } } @@ -224,10 +226,20 @@ impl Screening { let client = nostr.read(cx).client(); let public_key = self.public_key; + let Some(signer) = nostr.read(cx).signer(cx) else { + return; + }; + let task: Task> = cx.background_spawn(async move { - let tag = Tag::public_key_report(public_key, Report::Impersonation); - let builder = EventBuilder::report(vec![tag], ""); - let event = client.sign_event_builder(builder).await?; + let tag = Nip56Tag::PublicKey { + public_key, + report: Report::Impersonation, + } + .to_tag(); + + let event = EventBuilder::report(vec![tag], "") + .finalize_async(&signer) + .await?; // Send the report to the public relays client.send_event(&event).to(BOOTSTRAP_RELAYS).await?; diff --git a/desktop/src/panels/contact_list.rs b/desktop/src/panels/contact_list.rs index a9f86f2..7db2a29 100644 --- a/desktop/src/panels/contact_list.rs +++ b/desktop/src/panels/contact_list.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use std::time::Duration; -use anyhow::{Context as AnyhowContext, Error}; +use anyhow::Error; use gpui::prelude::FluentBuilder; use gpui::{ AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, @@ -82,11 +82,12 @@ impl ContactListPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let task: Task, Error>> = cx.background_spawn(async move { - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; - let contact_list = client.database().contacts_public_keys(public_key).await?; + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; + let task: Task, Error>> = cx.background_spawn(async move { + let contact_list = client.database().contacts_public_keys(public_key).await?; Ok(contact_list) }); @@ -157,6 +158,10 @@ impl ContactListPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let Some(signer) = nostr.read(cx).signer(cx) else { + return; + }; + // Get contacts let contacts: Vec = self .contacts @@ -169,8 +174,9 @@ impl ContactListPanel { let task: Task> = cx.background_spawn(async move { // Construct contact list event builder - let builder = EventBuilder::contact_list(contacts); - let event = client.sign_event_builder(builder).await?; + let event = ContactListBuilder::new(contacts) + .finalize_async(&signer) + .await?; // Set contact list client.send_event(&event).to_nip65().await?; diff --git a/desktop/src/panels/greeter.rs b/desktop/src/panels/greeter.rs index 97e1c37..07e973c 100644 --- a/desktop/src/panels/greeter.rs +++ b/desktop/src/panels/greeter.rs @@ -30,9 +30,8 @@ impl GreeterPanel { fn add_profile_panel(&mut self, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); - if let Some(public_key) = signer.public_key() { + if let Some(public_key) = nostr.read(cx).signer_pubkey(cx) { cx.spawn_in(window, async move |_this, cx| { cx.update(|window, cx| { Workspace::add_panel( diff --git a/desktop/src/panels/messaging_relays.rs b/desktop/src/panels/messaging_relays.rs index 1125c13..284c92c 100644 --- a/desktop/src/panels/messaging_relays.rs +++ b/desktop/src/panels/messaging_relays.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use std::time::Duration; -use anyhow::{Context as AnyhowContext, Error, anyhow}; +use anyhow::{Error, anyhow}; use gpui::prelude::FluentBuilder; use gpui::{ AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, @@ -83,17 +83,18 @@ impl MessagingRelayPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let task: Task, Error>> = cx.background_spawn(async move { - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; + let task: Task, Error>> = cx.background_spawn(async move { let filter = Filter::new() .kind(Kind::InboxRelays) .author(public_key) .limit(1); if let Some(event) = client.database().query(filter).await?.first_owned() { - Ok(nip17::extract_owned_relay_list(event).collect()) + Ok(nip17::extract_relay_list(&event).collect()) } else { Err(anyhow!("Not found.")) } @@ -171,11 +172,15 @@ impl MessagingRelayPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let Some(signer) = nostr.read(cx).signer(cx) else { + return; + }; + // Construct event tags let tags: Vec = self .relays .iter() - .map(|relay| Tag::relay(relay.clone())) + .map(|relay| Nip17Tag::Relay(relay.to_owned()).to_tag()) .collect(); // Set updating state @@ -183,8 +188,10 @@ impl MessagingRelayPanel { let task: Task> = cx.background_spawn(async move { // Construct nip17 event builder - let builder = EventBuilder::new(Kind::InboxRelays, "").tags(tags); - let event = client.sign_event_builder(builder).await?; + let event = EventBuilder::new(Kind::InboxRelays, "") + .tags(tags) + .finalize_async(&signer) + .await?; // Set messaging relays client.send_event(&event).to_nip65().await?; diff --git a/desktop/src/panels/profile.rs b/desktop/src/panels/profile.rs index 606b6ae..6b70ace 100644 --- a/desktop/src/panels/profile.rs +++ b/desktop/src/panels/profile.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use std::time::Duration; -use anyhow::{Context as AnyhowContext, Error}; +use anyhow::{Context as AnyhowContext, Error, anyhow}; use gpui::{ AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, Styled, Task, @@ -209,10 +209,15 @@ impl ProfilePanel { let client = nostr.read(cx).client(); let metadata = metadata.clone(); + let Some(signer) = nostr.read(cx).signer(cx) else { + return Task::ready(Err(anyhow!("Signer is required"))); + }; + cx.background_spawn(async move { // Build and sign the metadata event - let builder = EventBuilder::metadata(&metadata); - let event = client.sign_event_builder(builder).await?; + let event = EventBuilder::metadata(&metadata) + .finalize_async(&signer) + .await?; // Send event to user's relays client.send_event(&event).await?; diff --git a/desktop/src/panels/relay_list.rs b/desktop/src/panels/relay_list.rs index 8e8dfb8..26d4237 100644 --- a/desktop/src/panels/relay_list.rs +++ b/desktop/src/panels/relay_list.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use std::time::Duration; -use anyhow::{Context as AnyhowContext, Error, anyhow}; +use anyhow::{Error, anyhow}; use gpui::prelude::FluentBuilder; use gpui::{ Action, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, @@ -100,18 +100,19 @@ impl RelayListPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; + let task: Task)>, Error>> = cx .background_spawn(async move { - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; - let filter = Filter::new() .kind(Kind::RelayList) .author(public_key) .limit(1); if let Some(event) = client.database().query(filter).await?.first_owned() { - Ok(nip65::extract_owned_relay_list(event).collect()) + Ok(nip65::extract_relay_list(&event).collect()) } else { Err(anyhow!("Not found.")) } @@ -207,6 +208,10 @@ impl RelayListPanel { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let Some(signer) = nostr.read(cx).signer(cx) else { + return; + }; + // Get all relays let relays = self.relays.clone(); @@ -214,8 +219,9 @@ impl RelayListPanel { self.set_updating(true, cx); let task: Task> = cx.background_spawn(async move { - let builder = EventBuilder::relay_list(relays); - let event = client.sign_event_builder(builder).await?; + let event = EventBuilder::relay_list(relays) + .finalize_async(&signer) + .await?; // Set relay list for current user client.send_event(&event).await?; diff --git a/desktop/src/sidebar/mod.rs b/desktop/src/sidebar/mod.rs index fb74388..fa5598f 100644 --- a/desktop/src/sidebar/mod.rs +++ b/desktop/src/sidebar/mod.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; use std::ops::Range; use std::time::Duration; -use anyhow::{Context as AnyhowContext, Error}; +use anyhow::Error; use chat::{ChatEvent, ChatRegistry, Room, RoomKind}; use common::{DebouncedDelay, TimestampExt, coop_cache}; use entry::RoomEntry; @@ -159,11 +159,12 @@ impl Sidebar { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let task: Task, Error>> = cx.background_spawn(async move { - let signer = client.signer().context("Signer not found")?; - let public_key = signer.get_public_key().await?; - let contacts = client.database().contacts_public_keys(public_key).await?; + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; + let task: Task, Error>> = cx.background_spawn(async move { + let contacts = client.database().contacts_public_keys(public_key).await?; Ok(contacts) }); @@ -319,14 +320,14 @@ impl Sidebar { let async_chat = chat.downgrade(); let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; // Get all selected public keys let receivers = self.get_selected(cx); self.tasks.push(cx.spawn_in(window, async move |this, cx| { - let public_key = signer.get_public_key().await?; - // Create a new room and emit it async_chat.update_in(cx, |this, _window, cx| { let room = cx.new(|_| { diff --git a/desktop/src/workspace.rs b/desktop/src/workspace.rs index 0ef6318..985ae5d 100644 --- a/desktop/src/workspace.rs +++ b/desktop/src/workspace.rs @@ -24,23 +24,17 @@ use ui::menu::{DropdownMenu, PopupMenuItem}; use ui::notification::{Notification, NotificationKind}; use ui::{Icon, IconName, Root, Sizable, WindowExtension, h_flex, v_flex}; +use crate::dialogs::import::ImportIdentity; use crate::dialogs::restore::RestoreEncryption; -use crate::dialogs::{accounts, settings}; +use crate::dialogs::settings; use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list, trash}; use crate::sidebar; -const PREPARE_MSG: &str = "Coop is preparing a new identity for you. This may take a moment..."; -const ENC_MSG: &str = "Encryption Key is a special key that used to encrypt and decrypt your messages. \ - Your identity is completely decoupled from all encryption processes to protect your privacy."; -const ENC_WARN: &str = "By resetting your encryption key, you will lose access to \ - all your encrypted messages before. This action cannot be undone."; - pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Workspace::new(window, cx)) } struct DeviceNotifcation; -struct SignerNotifcation; struct RelayNotifcation; struct MsgRelayNotification; @@ -48,7 +42,6 @@ struct MsgRelayNotification; #[action(namespace = workspace, no_json)] enum Command { ToggleTheme, - ToggleAccount, RefreshMessagingRelays, BackupEncryption, @@ -75,7 +68,7 @@ pub struct Workspace { image_cache: Entity, /// Event subscriptions - _subscriptions: SmallVec<[Subscription; 5]>, + _subscriptions: SmallVec<[Subscription; 6]>, } impl Workspace { @@ -83,6 +76,7 @@ impl Workspace { let chat = ChatRegistry::global(cx); let device = DeviceRegistry::global(cx); let nostr = NostrRegistry::global(cx); + let signer = nostr.read(cx).signer.clone(); let titlebar = cx.new(|_| TitleBar::new()); let dock = cx.new(|cx| DockArea::new(window, cx)); @@ -98,19 +92,20 @@ impl Workspace { ); subscriptions.push( - // Subscribe to the signer events - cx.subscribe_in(&nostr, window, move |this, _state, event, window, cx| { - match event { - StateEvent::Creating => { - let note = Notification::new() - .id::() - .title("Preparing a new identity") - .message(PREPARE_MSG) - .autohide(false) - .with_kind(NotificationKind::Info); + // Observe the signer + cx.observe_in(&signer, window, |this, signer, window, cx| { + if signer.read(cx).is_some() { + this.set_center_layout(window, cx); + } else { + this.import_identity(window, cx); + } + }), + ); - window.push_notification(note, cx); - } + subscriptions.push( + // Subscribe to the nostr events + cx.subscribe_in(&nostr, window, move |this, state, event, window, cx| { + match event { StateEvent::Connecting => { let note = Notification::new() .id::() @@ -126,14 +121,10 @@ impl Workspace { .with_kind(NotificationKind::Success); window.push_notification(note, cx); - } - StateEvent::SignerSet => { - this.set_center_layout(window, cx); - // Clear the signer notification - window.clear_notification::(cx); - } - StateEvent::Show => { - this.account_selector(window, cx); + + if state.read(cx).signer.read(cx).is_none() { + this.import_identity(window, cx); + } } _ => {} }; @@ -341,9 +332,8 @@ impl Workspace { } Command::ShowProfile => { let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); - if let Some(public_key) = signer.public_key() { + if let Some(public_key) = nostr.read(cx).signer_pubkey(cx) { self.dock.update(cx, |this, cx| { this.add_panel( Arc::new(profile::init(public_key, window, cx)), @@ -413,9 +403,6 @@ impl Workspace { Command::ToggleTheme => { self.theme_selector(window, cx); } - Command::ToggleAccount => { - self.account_selector(window, cx); - } Command::BackupEncryption => { let device = DeviceRegistry::global(cx).downgrade(); let save_dialog = cx.prompt_for_new_path(download_dir(), Some("encryption.txt")); @@ -458,6 +445,12 @@ impl Workspace { } fn confirm_reset_encryption(&mut self, window: &mut Window, cx: &mut Context) { + const ENC_MSG: &str = "Encryption Key is a special key that used to encrypt and decrypt your messages. \ + Your identity is completely decoupled from all encryption processes to protect your privacy."; + + const ENC_WARN: &str = "By resetting your encryption key, you will lose access to \ + all your encrypted messages before. This action cannot be undone."; + let device = DeviceRegistry::global(cx); let ent = device.downgrade(); @@ -492,24 +485,21 @@ impl Workspace { fn import_encryption(&mut self, window: &mut Window, cx: &mut Context) { let restore = cx.new(|cx| RestoreEncryption::new(window, cx)); - window.open_modal(cx, move |this, _window, _cx| { - this.width(px(520.)) + this.width(px(420.)) .title("Restore Encryption") .child(restore.clone()) }); } - fn account_selector(&mut self, window: &mut Window, cx: &mut Context) { - let accounts = accounts::init(window, cx); + fn import_identity(&mut self, window: &mut Window, cx: &mut Context) { + let import = cx.new(|cx| ImportIdentity::new(window, cx)); window.open_modal(cx, move |this, _window, _cx| { - this.width(px(520.)) - .title("Continue with") + this.width(px(420.)) .show_close(false) - .keyboard(false) - .overlay_closable(false) - .child(accounts.clone()) + .title("Import Identity") + .child(import.clone()) }); } @@ -595,8 +585,7 @@ impl Workspace { fn titlebar_left(&mut self, cx: &mut Context) -> impl IntoElement { let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); - let current_user = signer.public_key(); + let current_user = nostr.read(cx).signer_pubkey(cx); h_flex() .flex_shrink_0() @@ -606,7 +595,7 @@ impl Workspace { div() .text_xs() .text_color(cx.theme().text_muted) - .child(SharedString::from("Choose an account to continue...")), + .child(SharedString::from("Import your identity to continue")), ) }) .when_some(current_user.as_ref(), |this, public_key| { @@ -657,11 +646,6 @@ impl Workspace { Box::new(Command::ToggleTheme), ) .separator() - .menu_with_icon( - "Accounts", - IconName::Group, - Box::new(Command::ToggleAccount), - ) .menu_with_icon( "Settings", IconName::Settings, @@ -676,11 +660,9 @@ impl Workspace { let chat = ChatRegistry::global(cx); let trash_messages = chat.read(cx).count_trash_messages(cx); let is_nip4e_enabled = AppSettings::get_encryption_key(cx); - let nostr = NostrRegistry::global(cx); - let signer = nostr.read(cx).signer(); - let Some(public_key) = signer.public_key() else { + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { return div(); }; diff --git a/rust-toolchain.toml b/rust-toolchain.toml index a3f50e4..65beb3c 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.92" +channel = "1.96" profile = "minimal" components = ["rustfmt", "clippy"] targets = [ -- 2.49.1 From e812ae05a957bd11ccdfae593a67968087ad1324 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Wed, 10 Jun 2026 13:00:51 +0700 Subject: [PATCH 10/12] fix --- crates/chat/src/lib.rs | 19 +++++---- crates/chat_ui/src/lib.rs | 8 ++-- crates/device/src/lib.rs | 72 ++++++++++++++++++++++----------- crates/settings/src/lib.rs | 40 +++++++++--------- crates/state/src/lib.rs | 7 +--- desktop/src/dialogs/import.rs | 8 ++-- desktop/src/dialogs/settings.rs | 4 +- desktop/src/workspace.rs | 3 +- 8 files changed, 94 insertions(+), 67 deletions(-) diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs index fb38881..a1c135a 100644 --- a/crates/chat/src/lib.rs +++ b/crates/chat/src/lib.rs @@ -360,7 +360,6 @@ impl ChatRegistry { // Subscribe client .subscribe(vec![msg_relays, contact_list]) - .with_id(SubscriptionId::new("user-meta")) .close_on(opts) .await?; @@ -622,7 +621,13 @@ impl ChatRegistry { /// Load all rooms from the database. pub fn get_rooms(&mut self, cx: &mut Context) { - let task = self.get_rooms_from_database(cx); + let nostr = NostrRegistry::global(cx); + + let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { + return; + }; + + let task = self.get_rooms_from_database(public_key, cx); self.tasks.push(cx.spawn(async move |this, cx| { match task.await { @@ -642,14 +647,14 @@ impl ChatRegistry { } /// Create a task to load rooms from the database - fn get_rooms_from_database(&self, cx: &App) -> Task, Error>> { + fn get_rooms_from_database( + &self, + public_key: PublicKey, + cx: &App, + ) -> Task, Error>> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { - return Task::ready(Err(anyhow!("Signer is required"))); - }; - cx.background_spawn(async move { // Get contacts let contacts = client diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index 6b87781..cdc69eb 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -618,11 +618,11 @@ impl ChatPanel { } } Command::ChangeSigner(kind) => { - let is_nip4e_enabled = AppSettings::get_encryption_key(cx); + let settings = AppSettings::global(cx); + let is_nip4e_enabled = settings.read(cx).is_nip4e_enabled(cx); + let is_force_nip4e = *kind == SignerKind::Encryption || *kind == SignerKind::Auto; - if !is_nip4e_enabled - && (*kind == SignerKind::Encryption || *kind == SignerKind::Auto) - { + if !is_nip4e_enabled && is_force_nip4e { window.push_notification("Decoupling Encryption Key is not enabled", cx); return; } diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 1b1a462..6aeb16c 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -15,7 +15,7 @@ use nostr_sdk::prelude::*; use person::PersonRegistry; use settings::AppSettings; use smallvec::{SmallVec, smallvec}; -use state::{Announcement, CLIENT_NAME, NostrRegistry, StateEvent}; +use state::{Announcement, CLIENT_NAME, NostrRegistry}; use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::Button; @@ -91,6 +91,7 @@ impl DeviceRegistry { /// Create a new device registry instance fn new(window: &mut Window, cx: &mut Context) -> Self { let signer = cx.new(|_| None); + let nostr = NostrRegistry::global(cx); let user_signer = nostr.read(cx).signer.clone(); @@ -109,7 +110,7 @@ impl DeviceRegistry { ); subscriptions.push( - // Subscribe to nostr state events + // Observe the user signer cx.observe(&user_signer, move |this, signer, cx| { if signer.read(cx).is_some() && is_nip4e_enabled { this.get_announcement(cx); @@ -133,14 +134,11 @@ impl DeviceRegistry { fn handle_notifications(&mut self, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + let current_user = nostr.read(cx).signer_pubkey(cx); let announcement_existed = self.announcement_existed.clone(); let (tx, rx) = flume::bounded::(100); - let Some(current_user) = nostr.read(cx).signer_pubkey(cx) else { - return; - }; - self.tasks.push(cx.background_spawn(async move { let mut notifications = client.notifications(); let mut processed_events = HashSet::new(); @@ -155,21 +153,15 @@ impl DeviceRegistry { } match event.kind { - Kind::Custom(10044) => { - if current_user == event.pubkey { - announcement_existed.store(true, Ordering::Relaxed); - tx.send_async(event.into_owned()).await?; - } + Kind::Custom(10044) if current_user == Some(event.pubkey) => { + announcement_existed.store(true, Ordering::Relaxed); + tx.send_async(event.into_owned()).await?; } - Kind::Custom(4454) => { - if current_user == event.pubkey { - tx.send_async(event.into_owned()).await?; - } + Kind::Custom(4454) if current_user == Some(event.pubkey) => { + tx.send_async(event.into_owned()).await?; } - Kind::Custom(4455) => { - if current_user == event.pubkey { - tx.send_async(event.into_owned()).await?; - } + Kind::Custom(4455) if current_user == Some(event.pubkey) => { + tx.send_async(event.into_owned()).await?; } _ => {} } @@ -426,14 +418,16 @@ impl DeviceRegistry { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let app_keys = Keys::generate(); - let app_pubkey = app_keys.public_key(); - let Some(signer) = nostr.read(cx).signer(cx) else { return; }; + let Ok(app_keys) = get_or_init_app_keys(cx) else { + return; + }; + let task: Task, Error>> = cx.background_spawn(async move { + let app_pubkey = app_keys.public_key(); let public_key = signer.get_public_key_async().await?; // Construct a filter to get the latest approval event @@ -516,8 +510,9 @@ impl DeviceRegistry { /// Parse the approval event to get encryption key then set it fn extract_encryption(&mut self, event: Event, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - let app_keys = Keys::generate(); + let Ok(app_keys) = get_or_init_app_keys(cx) else { + return; + }; let task: Task> = cx.background_spawn(async move { let master = event @@ -744,6 +739,35 @@ impl DeviceRegistry { struct DeviceNotification; +/// Get or create new app keys +fn get_or_init_app_keys(cx: &App) -> Result { + let read = cx.read_credentials(CLIENT_NAME); + let stored_keys: Option = cx.foreground_executor().block_on(async move { + if let Ok(Some((_, secret))) = read.await { + SecretKey::from_slice(&secret).map(Keys::new).ok() + } else { + None + } + }); + + if let Some(keys) = stored_keys { + Ok(keys) + } else { + let keys = Keys::generate(); + let user = keys.public_key().to_hex(); + let secret = keys.secret_key().to_secret_bytes(); + let write = cx.write_credentials(CLIENT_NAME, &user, &secret); + + cx.foreground_executor().block_on(async move { + if let Err(e) = write.await { + log::error!("Keyring not available or panic: {e}") + } + }); + + Ok(keys) + } +} + /// Encrypt and store device keys in the local database. async fn set_keys(client: &Client, signer: &Keys, secret: &str) -> Result<(), Error> { let public_key = signer.get_public_key_async().await?; diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index 9651e55..e88660f 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -1,4 +1,3 @@ -use std::collections::{HashMap, HashSet}; use std::fmt::Display; use std::rc::Rc; @@ -42,10 +41,9 @@ setting_accessors! { pub theme_mode: ThemeMode, pub hide_avatar: bool, pub screening: bool, - pub encryption_key: bool, + pub nip4e: bool, pub auth_mode: AuthMode, - pub trusted_relays: HashSet, - pub room_configs: HashMap, + pub trusted_relays: Vec, pub file_server: Url, } @@ -141,18 +139,13 @@ pub struct Settings { pub screening: bool, /// Enable decoupling encryption key - /// - /// NIP-4e - pub encryption_key: bool, + pub nip4e: bool, /// Authentication mode pub auth_mode: AuthMode, /// Trusted relays; Coop will automatically authenticate with these relays - pub trusted_relays: HashSet, - - /// Configuration for each chat room - pub room_configs: HashMap, + pub trusted_relays: Vec, /// Server for blossom media attachments pub file_server: Url, @@ -165,10 +158,9 @@ impl Default for Settings { theme_mode: ThemeMode::default(), hide_avatar: false, screening: true, - encryption_key: false, + nip4e: false, auth_mode: AuthMode::default(), - trusted_relays: HashSet::default(), - room_configs: HashMap::default(), + trusted_relays: vec![], file_server: Url::parse("https://blossom.band/").unwrap(), } } @@ -315,21 +307,29 @@ impl AppSettings { /// Check if decoupling encryption key is enabled pub fn is_nip4e_enabled(&self, cx: &App) -> bool { - self.inner.read(cx).encryption_key + self.inner.read(cx).nip4e } /// Check if the given relay is already authenticated pub fn trusted_relay(&self, url: &RelayUrl, cx: &App) -> bool { - self.inner.read(cx).trusted_relays.iter().any(|relay| { - relay.as_str_without_trailing_slash() == url.as_str_without_trailing_slash() - }) + self.inner + .read(cx) + .trusted_relays + .iter() + .any(|relay| relay == url.as_str_without_trailing_slash()) } /// Add a relay to the trusted list pub fn add_trusted_relay(&mut self, url: &RelayUrl, cx: &mut Context) { self.inner.update(cx, |this, cx| { - this.trusted_relays.insert(url.clone()); - cx.notify(); + if !this + .trusted_relays + .iter() + .any(|relay| relay == url.as_str_without_trailing_slash()) + { + this.trusted_relays.push(url.to_string()); + cx.notify(); + } }); } } diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 0546279..7c92d98 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -98,12 +98,7 @@ impl NostrRegistry { let client = ClientBuilder::default() .database(lmdb) .gossip(NostrGossipMemory::unbounded()) - .gossip_config( - GossipConfig::default() - .sync_initial_timeout(Duration::from_millis(100)) - .sync_idle_timeout(Duration::from_millis(100)) - .no_background_refresh(), - ) + .gossip_config(GossipConfig::default().no_background_refresh()) .connect_timeout(Duration::from_secs(10)) .sleep_when_idle(SleepWhenIdle::Enabled { timeout: Duration::from_secs(600), diff --git a/desktop/src/dialogs/import.rs b/desktop/src/dialogs/import.rs index ad2347b..0b690c6 100644 --- a/desktop/src/dialogs/import.rs +++ b/desktop/src/dialogs/import.rs @@ -11,7 +11,7 @@ use state::NostrRegistry; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::input::{Input, InputEvent, InputState}; -use ui::{Disableable, v_flex}; +use ui::{Disableable, WindowExtension, v_flex}; #[derive(Debug)] pub struct ImportIdentity { @@ -74,6 +74,7 @@ impl ImportIdentity { nostr.update(cx, |this, cx| { this.set_signer(keys, cx); }); + window.close_modal(cx); } else { self.set_error("Invalid key", cx); } @@ -109,9 +110,10 @@ impl ImportIdentity { self.tasks.push(cx.spawn_in(window, async move |this, cx| { match task.await { Ok(keys) => { - nostr.update(cx, |this, cx| { + nostr.update_in(cx, |this, window, cx| { this.set_signer(keys, cx); - }); + window.close_modal(cx); + })?; } Err(e) => { this.update(cx, |this, cx| { diff --git a/desktop/src/dialogs/settings.rs b/desktop/src/dialogs/settings.rs index 408f0c6..5bf9081 100644 --- a/desktop/src/dialogs/settings.rs +++ b/desktop/src/dialogs/settings.rs @@ -65,7 +65,7 @@ impl Render for Preferences { let screening = AppSettings::get_screening(cx); let hide_avatar = AppSettings::get_hide_avatar(cx); - let nip4e = AppSettings::get_encryption_key(cx); + let nip4e = AppSettings::get_nip4e(cx); let auth_mode = AppSettings::get_auth_mode(cx); let theme_mode = AppSettings::get_theme_mode(cx); @@ -217,7 +217,7 @@ impl Render for Preferences { .description(NIP4E) .checked(nip4e) .on_click(move |_, _window, cx| { - AppSettings::update_encryption_key(!nip4e, cx); + AppSettings::update_nip4e(!nip4e, cx); }), ), ) diff --git a/desktop/src/workspace.rs b/desktop/src/workspace.rs index 985ae5d..6303190 100644 --- a/desktop/src/workspace.rs +++ b/desktop/src/workspace.rs @@ -659,7 +659,8 @@ impl Workspace { fn titlebar_right(&mut self, cx: &mut Context) -> impl IntoElement { let chat = ChatRegistry::global(cx); let trash_messages = chat.read(cx).count_trash_messages(cx); - let is_nip4e_enabled = AppSettings::get_encryption_key(cx); + + let is_nip4e_enabled = AppSettings::get_nip4e(cx); let nostr = NostrRegistry::global(cx); let Some(public_key) = nostr.read(cx).signer_pubkey(cx) else { -- 2.49.1 From 5edc8d311e4b451846c49b26bdd26c934374a0c4 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Wed, 10 Jun 2026 14:37:50 +0700 Subject: [PATCH 11/12] group message --- Cargo.lock | 72 +++++----- crates/chat/src/message.rs | 268 ++++++++++++++----------------------- crates/chat_ui/src/lib.rs | 188 +++++++++++++++----------- 3 files changed, 244 insertions(+), 284 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 03dfff4..08f173c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -247,9 +247,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "ar_archive_writer" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +checksum = "4087686b4b0a3427190bae57a1d9a478dbb2d40c5dc1bd6e2b6d797913bdd348" dependencies = [ "object", ] @@ -1369,7 +1369,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "gpui_util", "indexmap", @@ -1891,7 +1891,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "proc-macro2", "quote", @@ -2865,7 +2865,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "accesskit", "anyhow", @@ -2947,7 +2947,7 @@ dependencies = [ [[package]] name = "gpui_linux" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "accesskit", "accesskit_unix", @@ -2998,7 +2998,7 @@ dependencies = [ [[package]] name = "gpui_macos" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "accesskit", "accesskit_macos", @@ -3043,7 +3043,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -3054,7 +3054,7 @@ dependencies = [ [[package]] name = "gpui_platform" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "console_error_panic_hook", "gpui", @@ -3067,7 +3067,7 @@ dependencies = [ [[package]] name = "gpui_shared_string" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "schemars", "serde", @@ -3077,7 +3077,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "anyhow", "gpui", @@ -3088,7 +3088,7 @@ dependencies = [ [[package]] name = "gpui_util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "anyhow", "log", @@ -3097,7 +3097,7 @@ dependencies = [ [[package]] name = "gpui_web" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "anyhow", "console_error_panic_hook", @@ -3121,7 +3121,7 @@ dependencies = [ [[package]] name = "gpui_wgpu" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "anyhow", "bytemuck", @@ -3150,7 +3150,7 @@ dependencies = [ [[package]] name = "gpui_windows" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "accesskit", "accesskit_windows", @@ -3439,7 +3439,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "anyhow", "async-compression", @@ -3464,7 +3464,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "rustls", "rustls-platform-verifier", @@ -4322,7 +4322,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "anyhow", "bindgen", @@ -5225,7 +5225,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "collections", "serde", @@ -6018,16 +6018,16 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "derive_refineable", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -6048,9 +6048,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "relay_auth" @@ -6118,7 +6118,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "anyhow", "bytes", @@ -6433,7 +6433,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "async-task", "backtrace", @@ -7077,7 +7077,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "heapless 0.9.3", "log", @@ -8071,7 +8071,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "anyhow", "async-fs", @@ -8110,7 +8110,7 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "perf", "quote", @@ -9875,18 +9875,18 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.50" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.50" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", @@ -9970,7 +9970,7 @@ dependencies = [ [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "anyhow", "chrono", @@ -9987,7 +9987,7 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" dependencies = [ "tracing", "tracing-subscriber", @@ -9998,7 +9998,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#e5052961af01e6810f961d6b217376edbe02b106" +source = "git+https://github.com/zed-industries/zed#d989c7c5cdd057de2375a55bdc109ff61409801c" [[package]] name = "zune-core" diff --git a/crates/chat/src/message.rs b/crates/chat/src/message.rs index 10a8ba6..1d9217c 100644 --- a/crates/chat/src/message.rs +++ b/crates/chat/src/message.rs @@ -5,6 +5,106 @@ use common::{EventExt, NostrParser, extract_and_remove_media_urls}; use gpui::{SharedString, SharedUri}; use nostr_sdk::prelude::*; +/// Rendered message. +#[derive(Debug, Clone)] +pub struct Message { + pub id: EventId, + /// Author's public key + pub author: PublicKey, + /// The content/text of the message + pub content: String, + /// List of media URLs in the message + pub media: Vec, + /// Message created time as unix timestamp + pub created_at: Timestamp, + /// List of mentioned public keys in the message + pub mentions: Vec, + /// List of event of the message this message is a reply to + pub replies_to: Vec, +} + +impl From<&Event> for Message { + fn from(val: &Event) -> Self { + let mentions = extract_mentions(&val.content); + let replies_to = extract_reply_ids(&val.tags); + let (media, string) = extract_and_remove_media_urls(&val.content); + + Self { + id: val.id, + author: val.pubkey, + content: string, + media, + created_at: val.created_at, + mentions, + replies_to, + } + } +} + +impl From<&UnsignedEvent> for Message { + fn from(val: &UnsignedEvent) -> Self { + let mentions = extract_mentions(&val.content); + let replies_to = extract_reply_ids(&val.tags); + let (media, string) = extract_and_remove_media_urls(&val.content); + + Self { + // Event ID must be known + id: val.id.unwrap(), + author: val.pubkey, + content: string, + media, + created_at: val.created_at, + mentions, + replies_to, + } + } +} + +impl From<&NewMessage> for Message { + fn from(val: &NewMessage) -> Self { + let mentions = extract_mentions(&val.rumor.content); + let replies_to = extract_reply_ids(&val.rumor.tags); + let (media, string) = extract_and_remove_media_urls(&val.rumor.content); + + Self { + // Event ID must be known + id: val.rumor.id.unwrap(), + author: val.rumor.pubkey, + content: string, + media, + created_at: val.rumor.created_at, + mentions, + replies_to, + } + } +} + +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 { + Some(self.cmp(other)) + } +} + +impl Hash for Message { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + /// New message. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct NewMessage { @@ -44,74 +144,6 @@ impl FailedMessage { } } -/// Message. -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub enum Message { - User(RenderedMessage), - Warning(String, Timestamp), - System(Timestamp), -} - -impl Message { - pub fn user(user: I) -> Self - where - I: Into, - { - Self::User(user.into()) - } - - pub fn warning(content: I) -> Self - where - I: Into, - { - Self::Warning(content.into(), Timestamp::now()) - } - - pub fn system() -> Self { - Self::System(Timestamp::default()) - } - - fn timestamp(&self) -> &Timestamp { - match self { - Message::User(msg) => &msg.created_at, - Message::Warning(_, ts) => ts, - Message::System(ts) => ts, - } - } -} - -impl From<&NewMessage> for Message { - fn from(val: &NewMessage) -> Self { - Self::User(val.into()) - } -} - -impl From<&UnsignedEvent> for Message { - fn from(val: &UnsignedEvent) -> Self { - Self::User(val.into()) - } -} - -impl Ord for Message { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - match (self, other) { - // System always comes first - (Message::System(_), Message::System(_)) => self.timestamp().cmp(other.timestamp()), - (Message::System(_), _) => std::cmp::Ordering::Less, - (_, Message::System(_)) => std::cmp::Ordering::Greater, - - // For non-system messages, compare by timestamp - _ => self.timestamp().cmp(other.timestamp()), - } - } -} - -impl PartialOrd for Message { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - #[derive(Debug, Clone)] pub struct Mention { pub public_key: PublicKey, @@ -124,106 +156,6 @@ impl Mention { } } -/// Rendered message. -#[derive(Debug, Clone)] -pub struct RenderedMessage { - pub id: EventId, - /// Author's public key - pub author: PublicKey, - /// The content/text of the message - pub content: String, - /// List of media URLs in the message - pub media: Vec, - /// Message created time as unix timestamp - pub created_at: Timestamp, - /// List of mentioned public keys in the message - pub mentions: Vec, - /// List of event of the message this message is a reply to - pub replies_to: Vec, -} - -impl From<&Event> for RenderedMessage { - fn from(val: &Event) -> Self { - let mentions = extract_mentions(&val.content); - let replies_to = extract_reply_ids(&val.tags); - let (media, string) = extract_and_remove_media_urls(&val.content); - - Self { - id: val.id, - author: val.pubkey, - content: string, - media, - created_at: val.created_at, - mentions, - replies_to, - } - } -} - -impl From<&UnsignedEvent> for RenderedMessage { - fn from(val: &UnsignedEvent) -> Self { - let mentions = extract_mentions(&val.content); - let replies_to = extract_reply_ids(&val.tags); - let (media, string) = extract_and_remove_media_urls(&val.content); - - Self { - // Event ID must be known - id: val.id.unwrap(), - author: val.pubkey, - content: string, - media, - created_at: val.created_at, - mentions, - replies_to, - } - } -} - -impl From<&NewMessage> for RenderedMessage { - fn from(val: &NewMessage) -> Self { - let mentions = extract_mentions(&val.rumor.content); - let replies_to = extract_reply_ids(&val.rumor.tags); - let (media, string) = extract_and_remove_media_urls(&val.rumor.content); - - Self { - // Event ID must be known - id: val.rumor.id.unwrap(), - author: val.rumor.pubkey, - content: string, - media, - created_at: val.rumor.created_at, - mentions, - replies_to, - } - } -} - -impl Eq for RenderedMessage {} - -impl PartialEq for RenderedMessage { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } -} - -impl Ord for RenderedMessage { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.created_at.cmp(&other.created_at) - } -} - -impl PartialOrd for RenderedMessage { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Hash for RenderedMessage { - fn hash(&self, state: &mut H) { - self.id.hash(state); - } -} - /// Extracts all mentions (public keys) from a content string. fn extract_mentions(content: &str) -> Vec { let parser = NostrParser::new(); diff --git a/crates/chat_ui/src/lib.rs b/crates/chat_ui/src/lib.rs index cdc69eb..624c6f5 100644 --- a/crates/chat_ui/src/lib.rs +++ b/crates/chat_ui/src/lib.rs @@ -3,7 +3,7 @@ use std::sync::Arc; pub use actions::*; use anyhow::{Context as AnyhowContext, Error}; -use chat::{ChatRegistry, Message, RenderedMessage, Room, RoomEvent, SendReport, SendStatus}; +use chat::{ChatRegistry, Message, Room, RoomEvent, SendReport, SendStatus}; use common::{TimestampExt, coop_cache}; use gpui::prelude::FluentBuilder; use gpui::{ @@ -38,9 +38,6 @@ use crate::text::RenderedText; mod actions; mod text; -const ANNOUNCEMENT: &str = - "This conversation is private. Only members can see each other's messages."; - pub fn init(room: WeakEntity, window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| ChatPanel::new(room, window, cx)) } @@ -101,7 +98,7 @@ impl ChatPanel { let reports_by_id = cx.new(|_| BTreeMap::new()); // Define list of messages - let messages = BTreeSet::from([Message::system()]); + let messages = BTreeSet::default(); let list_state = ListState::new(messages.len(), ListAlignment::Bottom, px(1024.)); // Get room id and name @@ -476,25 +473,13 @@ impl ChatPanel { } /// Get a message by its ID - fn message(&self, id: &EventId) -> Option<&RenderedMessage> { - self.messages.iter().find_map(|msg| { - if let Message::User(rendered) = msg - && &rendered.id == id - { - return Some(rendered); - } - None - }) + fn message(&self, id: &EventId) -> Option<&Message> { + self.messages.iter().find(|msg| &msg.id == id) } - fn scroll_to(&self, id: EventId) { - if let Some(ix) = self.messages.iter().position(|m| { - if let Message::User(msg) = m { - msg.id == id - } else { - false - } - }) { + /// Scroll to a message by its ID + fn scroll_to(&self, id: &EventId) { + if let Some(ix) = self.messages.iter().position(|msg| &msg.id == id) { self.list_state.scroll_to_reveal_item(ix); } } @@ -742,9 +727,11 @@ impl ChatPanel { cx.open_url(&content); } - fn render_announcement(&self, ix: usize, cx: &Context) -> AnyElement { + fn render_announcement(&self, cx: &Context) -> AnyElement { + const MSG: &str = + "This conversation is private. Only members can see each other's messages."; + v_flex() - .id(ix) .h_40() .w_full() .gap_3() @@ -761,7 +748,7 @@ impl ChatPanel { .size_12() .text_color(cx.theme().ghost_element_active), ) - .child(SharedString::from(ANNOUNCEMENT)) + .child(MSG) .into_any_element() } @@ -798,6 +785,34 @@ impl ChatPanel { .into_any_element() } + fn is_group_start(&self, ix: usize) -> bool { + // 5 minutes + const GROUP_WINDOW: u64 = 300; + + if ix == 0 { + return true; + } + + let mut iter = self.messages.iter(); + + if let Some(previous) = iter.nth(ix - 1) + && let Some(current) = iter.next() + { + if current.author != previous.author { + return true; + } + + let gap = current + .created_at + .as_secs() + .saturating_sub(previous.created_at.as_secs()); + + return gap > GROUP_WINDOW; + } + + false + } + fn render_message( &mut self, ix: usize, @@ -805,24 +820,17 @@ impl ChatPanel { cx: &mut Context, ) -> AnyElement { if let Some(message) = self.messages.iter().nth(ix) { - match message { - Message::User(rendered) => { - let persons = PersonRegistry::global(cx); - let text = self - .rendered_texts_by_id - .entry(rendered.id) - .or_insert_with(|| { - RenderedText::new(&rendered.content, &rendered.mentions, &persons, cx) - }) - .element(ix.into(), window, cx); + let persons = PersonRegistry::global(cx); + let show_author = self.is_group_start(ix); + let text = self + .rendered_texts_by_id + .entry(message.id) + .or_insert_with(|| { + RenderedText::new(&message.content, &message.mentions, &persons, cx) + }) + .element(ix.into(), window, cx); - self.render_text_message(ix, rendered, text, cx) - } - Message::Warning(content, _timestamp) => { - self.render_warning(ix, SharedString::from(content), cx) - } - Message::System(_timestamp) => self.render_announcement(ix, cx), - } + self.render_text_message(ix, message, text, show_author, cx) } else { self.render_warning(ix, SharedString::from("Message not found"), cx) } @@ -831,8 +839,9 @@ impl ChatPanel { fn render_text_message( &self, ix: usize, - message: &RenderedMessage, + message: &Message, rendered_text: AnyElement, + show_author: bool, cx: &Context, ) -> AnyElement { let id = message.id; @@ -858,17 +867,21 @@ impl ChatPanel { .flex() .gap_3() .when(!hide_avatar, |this| { - this.child( - Avatar::new(author.avatar()) - .flex_shrink_0() - .relative() - .dropdown_menu(move |this, _window, _cx| { - this.menu("Public Key", Box::new(Command::Copy(pk))) - .menu("View Relays", Box::new(Command::Relays(pk))) - .separator() - .menu("View on njump.me", Box::new(Command::Njump(pk))) - }), - ) + if show_author { + this.child( + Avatar::new(author.avatar()) + .flex_shrink_0() + .relative() + .dropdown_menu(move |this, _window, _cx| { + this.menu("Public Key", Box::new(Command::Copy(pk))) + .menu("View Relays", Box::new(Command::Relays(pk))) + .separator() + .menu("View on njump.me", Box::new(Command::Njump(pk))) + }), + ) + } else { + this.child(div().flex_shrink_0().w(px(32.))) + } }) .child( v_flex() @@ -876,22 +889,24 @@ impl ChatPanel { .w_full() .flex_initial() .overflow_hidden() - .child( - h_flex() - .gap_2() - .text_sm() - .text_color(cx.theme().text_placeholder) - .child( - div() - .font_semibold() - .text_color(cx.theme().text) - .child(author.name()), - ) - .child(message.created_at.to_human_time()) - .when(has_reports, |this| { - this.child(self.render_sent_reports(&id, cx)) - }), - ) + .when(show_author, |this| { + this.child( + h_flex() + .gap_2() + .text_sm() + .text_color(cx.theme().text_placeholder) + .child( + div() + .font_semibold() + .text_color(cx.theme().text) + .child(author.name()), + ) + .child(message.created_at.to_human_time()) + .when(has_reports, |this| { + this.child(self.render_sent_reports(&id, cx)) + }), + ) + }) .when(has_replies, |this| { this.children(self.render_message_replies(replies, cx)) }) @@ -1009,7 +1024,7 @@ impl ChatPanel { .on_click({ let id = *id; cx.listener(move |this, _event, _window, _cx| { - this.scroll_to(id); + this.scroll_to(&id); }) }), ); @@ -1500,15 +1515,28 @@ impl Render for ChatPanel { v_flex() .flex_1() .relative() - .child( - list( - self.list_state.clone(), - cx.processor(move |this, ix, window, cx| { - this.render_message(ix, window, cx) - }), - ) - .size_full(), - ) + .map(|this| { + if self.messages.is_empty() { + this.child( + div() + .size_full() + .flex() + .items_center() + .justify_end() + .child(self.render_announcement(cx)), + ) + } else { + this.child( + list( + self.list_state.clone(), + cx.processor(move |this, ix, window, cx| { + this.render_message(ix, window, cx) + }), + ) + .size_full(), + ) + } + }) .child(Scrollbar::vertical(&self.list_state)), ) .child( -- 2.49.1 From e95cc5967fb0e2a36fbcecf30e75d296e4d8c041 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Wed, 10 Jun 2026 14:51:44 +0700 Subject: [PATCH 12/12] fix settings --- crates/relay_auth/src/lib.rs | 19 ++++++++----------- crates/settings/src/lib.rs | 3 ++- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/crates/relay_auth/src/lib.rs b/crates/relay_auth/src/lib.rs index 13ed5ca..8d32e61 100644 --- a/crates/relay_auth/src/lib.rs +++ b/crates/relay_auth/src/lib.rs @@ -282,18 +282,15 @@ impl RelayAuth { // Clear pending events for the authenticated relay this.clear_pending_events(url, cx); - // Only show the success notification if the relay was not already trusted - if !settings.read(cx).trusted_relay(url, cx) { - let domain = url.domain().unwrap_or_default(); - let msg = format!("Relay {} has been authenticated", domain); + let domain = url.domain().unwrap_or_default(); + let msg = format!("Relay {} has been authenticated", domain); - window.push_notification(Notification::success(msg), cx); - } else { - // Save the authenticated relay to automatically authenticate future requests - settings.update(cx, |this, cx| { - this.add_trusted_relay(url, cx); - }); - } + window.push_notification(Notification::success(msg), cx); + + // Save the authenticated relay to automatically authenticate future requests + settings.update(cx, |this, cx| { + this.add_trusted_relay(url, cx); + }); } }) .ok(); diff --git a/crates/settings/src/lib.rs b/crates/settings/src/lib.rs index e88660f..e450a1c 100644 --- a/crates/settings/src/lib.rs +++ b/crates/settings/src/lib.rs @@ -327,7 +327,8 @@ impl AppSettings { .iter() .any(|relay| relay == url.as_str_without_trailing_slash()) { - this.trusted_relays.push(url.to_string()); + this.trusted_relays + .push(url.as_str_without_trailing_slash().to_string()); cx.notify(); } }); -- 2.49.1