diff --git a/crates/coop/src/dialogs/import.rs b/crates/coop/src/dialogs/import.rs index e8c111f..205221b 100644 --- a/crates/coop/src/dialogs/import.rs +++ b/crates/coop/src/dialogs/import.rs @@ -242,7 +242,6 @@ impl Render for ImportKey { fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() - .p_4() .gap_2() .text_sm() .child( diff --git a/crates/coop/src/dialogs/mod.rs b/crates/coop/src/dialogs/mod.rs index e98dec8..831a2de 100644 --- a/crates/coop/src/dialogs/mod.rs +++ b/crates/coop/src/dialogs/mod.rs @@ -1,6 +1,6 @@ pub mod accounts; +pub mod connect; +pub mod import; +pub mod restore; pub mod screening; pub mod settings; - -mod connect; -mod import; diff --git a/crates/coop/src/dialogs/restore.rs b/crates/coop/src/dialogs/restore.rs new file mode 100644 index 0000000..cde7591 --- /dev/null +++ b/crates/coop/src/dialogs/restore.rs @@ -0,0 +1,130 @@ +use std::time::Duration; + +use anyhow::Error; +use device::DeviceRegistry; +use gpui::prelude::FluentBuilder; +use gpui::{ + AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled, + Subscription, Task, Window, div, +}; +use nostr_connect::prelude::*; +use theme::ActiveTheme; +use ui::button::{Button, ButtonVariants}; +use ui::input::{InputEvent, InputState, TextInput}; +use ui::{WindowExtension, v_flex}; + +#[derive(Debug)] +pub struct RestoreEncryption { + /// Secret key input + key_input: Entity, + + /// Error message + error: Entity>, + + /// Async tasks + tasks: Vec>>, + + /// Event subscription + _subscription: Option, +} + +impl RestoreEncryption { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let key_input = cx.new(|cx| InputState::new(window, cx).masked(true)); + let error = cx.new(|_| None); + + let subscription = + cx.subscribe_in(&key_input, window, |this, _input, event, window, cx| { + if let InputEvent::PressEnter { .. } = event { + this.restore(window, cx); + }; + }); + + Self { + key_input, + error, + tasks: vec![], + _subscription: Some(subscription), + } + } + + fn restore(&mut self, window: &mut Window, cx: &mut Context) { + let device = DeviceRegistry::global(cx); + let content = self.key_input.read(cx).value(); + + if !content.is_empty() { + self.set_error("Secret Key cannot be empty.", cx); + } + + let Ok(secret) = SecretKey::parse(&content) else { + self.set_error("Secret Key is invalid.", cx); + return; + }; + + device.update(cx, |this, cx| { + this.set_announcement(Keys::new(secret), cx); + }); + + // Close the current modal + window.close_modal(cx); + } + + fn set_error(&mut self, message: S, cx: &mut Context) + where + S: Into, + { + // Update error message + self.error.update(cx, |this, cx| { + *this = Some(message.into()); + cx.notify(); + }); + + // Clear the error message after 3 secs + self.tasks.push(cx.spawn(async move |this, cx| { + cx.background_executor().timer(Duration::from_secs(3)).await; + + this.update(cx, |this, cx| { + this.error.update(cx, |this, cx| { + *this = None; + cx.notify(); + }); + })?; + + Ok(()) + })); + } +} + +impl Render for RestoreEncryption { + fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context) -> impl IntoElement { + v_flex() + .size_full() + .gap_2() + .text_sm() + .child( + v_flex() + .gap_1() + .text_sm() + .text_color(cx.theme().text_muted) + .child("Secret Key") + .child(TextInput::new(&self.key_input)), + ) + .child( + Button::new("restore") + .label("Restore") + .primary() + .on_click(cx.listener(move |this, _, window, cx| { + this.restore(window, cx); + })), + ) + .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()), + ) + }) + } +} diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs index 3ee3a7f..472b01d 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -11,6 +11,7 @@ use gpui::{ Action, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window, div, px, }; +use nostr_sdk::prelude::*; use person::PersonRegistry; use serde::Deserialize; use smallvec::{SmallVec, smallvec}; @@ -24,6 +25,7 @@ use ui::menu::{DropdownMenu, PopupMenuItem}; use ui::notification::{Notification, NotificationKind}; use ui::{Disableable, IconName, Root, Sizable, WindowExtension, h_flex, v_flex}; +use crate::dialogs::restore::RestoreEncryption; use crate::dialogs::{accounts, settings}; use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list}; use crate::sidebar; @@ -147,7 +149,7 @@ impl Workspace { window.push_notification(note, cx); } StateEvent::RelayNotConfigured => { - this.relay_notification(window, cx); + this.relay_warning(window, cx); } StateEvent::RelayConnected => { window.clear_notification::(cx); @@ -167,13 +169,35 @@ impl Workspace { subscriptions.push( // Observe all events emitted by the device registry - cx.subscribe_in(&device, window, |_this, _device, ev, window, cx| { - match ev { + cx.subscribe_in(&device, window, |_this, _device, event, window, cx| { + match event { + DeviceEvent::Requesting => { + const MSG: &str = + "Please open the other client and approve the encryption key request"; + + let note = Notification::new() + .id::() + .title("Wait for approval") + .message(MSG) + .with_kind(NotificationKind::Info); + + window.push_notification(note, cx); + } + DeviceEvent::Creating => { + let note = Notification::new() + .id::() + .message("Creating encryption key") + .with_kind(NotificationKind::Info); + + window.push_notification(note, cx); + } DeviceEvent::Set => { - window.push_notification( - Notification::success("Encryption Key has been set"), - cx, - ); + let note = Notification::new() + .id::() + .message("Encryption Key has been set") + .with_kind(NotificationKind::Success); + + window.push_notification(note, cx); } DeviceEvent::NotSet { reason } => { let note = Notification::new() @@ -198,9 +222,6 @@ impl Workspace { DeviceEvent::Error(error) => { window.push_notification(Notification::error(error).autohide(false), cx); } - _ => { - // TODO - } }; }), ); @@ -466,15 +487,22 @@ impl Workspace { }) .detach(); } - _ => {} + Command::ImportEncryption => { + self.import_encryption(window, cx); + } } } fn confirm_reset_encryption(&mut self, window: &mut Window, cx: &mut Context) { - window.open_modal(cx, |this, _window, cx| { + let device = DeviceRegistry::global(cx); + let ent = device.downgrade(); + + window.open_modal(cx, move |this, _window, cx| { + let ent = ent.clone(); + this.confirm() .show_close(true) - .title("Reset Encryption Keys") + .title("Reset Encryption Key") .child( v_flex() .gap_1() @@ -488,16 +516,26 @@ impl Workspace { ), ) .on_ok(move |_ev, _window, cx| { - let device = DeviceRegistry::global(cx); - device.update(cx, |this, cx| { - this.set_announcement(cx); - }); + ent.update(cx, |this, cx| { + this.set_announcement(Keys::generate(), cx); + }) + .ok(); // true to close modal true }) }); } + 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.)) + .title("Restore Encryption") + .child(restore.clone()) + }); + } + fn account_selector(&mut self, window: &mut Window, cx: &mut Context) { let accounts = accounts::init(window, cx); @@ -507,7 +545,6 @@ impl Workspace { .show_close(false) .keyboard(false) .overlay_closable(false) - .pb_2() .child(accounts.clone()) }); } @@ -520,7 +557,6 @@ impl Workspace { this.width(px(520.)) .show_close(true) .title("Select theme") - .pb_2() .child(v_flex().gap_2().w_full().children({ let mut items = vec![]; @@ -593,7 +629,7 @@ impl Workspace { }); } - fn relay_notification(&mut self, window: &mut Window, cx: &mut Context) { + fn relay_warning(&mut self, window: &mut Window, cx: &mut Context) { const BODY: &str = "Coop cannot found your gossip relay list. \ Maybe you haven't set it yet or relay not responsed"; diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index ac5070b..5120b8d 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -37,6 +37,8 @@ pub enum DeviceEvent { Set, /// The device is requesting an encryption key Requesting, + /// The device is creating a new encryption key + Creating, /// Encryption key is not set NotSet { reason: SharedString }, /// An event to notify that Coop isn't subscribed to gift wrap events @@ -345,7 +347,7 @@ impl DeviceRegistry { Err(_) => { // User has no announcement, create a new one this.update(cx, |this, cx| { - this.set_announcement(cx); + this.set_announcement(Keys::generate(), cx); })?; } } @@ -355,8 +357,11 @@ impl DeviceRegistry { } /// Create a new device signer and announce it to user's relay list - pub fn set_announcement(&mut self, cx: &mut Context) { - let task = self.new_encryption(cx); + 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 { @@ -376,12 +381,11 @@ impl DeviceRegistry { })); } - /// Create new encryption keys - fn new_encryption(&self, cx: &App) -> Task> { + /// Create new encryption key and announce it to user's relay list + fn create_encryption(&self, keys: Keys, cx: &App) -> Task> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); - let keys = Keys::generate(); let secret = keys.secret_key().to_secret_hex(); let n = keys.public_key(); @@ -396,7 +400,11 @@ impl DeviceRegistry { let event = client.sign_event_builder(builder).await?; // Publish announcement - client.send_event(&event).to_nip65().await?; + client + .send_event(&event) + .to_nip65() + .ack_policy(AckPolicy::none()) + .await?; // Save device keys to the database set_keys(&client, &secret).await?;