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 {