From 40e7ca368b70ebcd5cd9f54e37c4310968e29cc1 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Tue, 17 Mar 2026 07:42:25 +0000 Subject: [PATCH] feat: add backup/restore for NIP-4e encryption key (#22) Reviewed-on: https://git.reya.su/reya/coop/pulls/22 Co-authored-by: Ren Amamiya Co-committed-by: Ren Amamiya --- Cargo.lock | 88 +++---- crates/common/src/paths.rs | 13 +- crates/coop/src/dialogs/import.rs | 1 - crates/coop/src/dialogs/mod.rs | 6 +- crates/coop/src/dialogs/restore.rs | 130 ++++++++++ crates/coop/src/workspace.rs | 225 ++++++++++++---- crates/device/src/lib.rs | 398 ++++++++++++++++------------- crates/state/src/device.rs | 34 --- 8 files changed, 576 insertions(+), 319 deletions(-) create mode 100644 crates/coop/src/dialogs/restore.rs diff --git a/Cargo.lock b/Cargo.lock index f71a337..c8e025e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -117,9 +117,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" @@ -220,9 +220,9 @@ dependencies = [ [[package]] name = "ashpd" -version = "0.13.5" +version = "0.13.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b09507a218cf6eb4ab0659883e54880cea3984e3dbaa4989b6cda3f8f8a97a5" +checksum = "313dc617cf7b7e5d58021f999756898e60bdddd64eab2bc2f67909659e3ce5f9" dependencies = [ "enumflags2", "futures-channel", @@ -914,9 +914,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.56" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ "find-msvc-tools", "jobserver", @@ -1200,7 +1200,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "indexmap", "rustc-hash 2.1.1", @@ -1214,9 +1214,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "combine" @@ -1659,7 +1659,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "proc-macro2", "quote", @@ -1857,9 +1857,9 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "embed-resource" -version = "3.0.6" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +checksum = "47ec73ddcf6b7f23173d5c3c5a32b5507dc0a734de7730aa14abc5d5e296bb5f" dependencies = [ "cc", "memchr", @@ -2650,7 +2650,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "anyhow", "async-channel 2.5.0", @@ -2729,7 +2729,7 @@ dependencies = [ [[package]] name = "gpui_linux" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2777,7 +2777,7 @@ dependencies = [ [[package]] name = "gpui_macos" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "anyhow", "async-task", @@ -2819,7 +2819,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -2830,7 +2830,7 @@ dependencies = [ [[package]] name = "gpui_platform" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "console_error_panic_hook", "gpui", @@ -2843,7 +2843,7 @@ dependencies = [ [[package]] name = "gpui_tokio" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "anyhow", "gpui", @@ -2854,7 +2854,7 @@ dependencies = [ [[package]] name = "gpui_util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "anyhow", "log", @@ -2863,7 +2863,7 @@ dependencies = [ [[package]] name = "gpui_web" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "anyhow", "console_error_panic_hook", @@ -2887,7 +2887,7 @@ dependencies = [ [[package]] name = "gpui_wgpu" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "anyhow", "bytemuck", @@ -2915,7 +2915,7 @@ dependencies = [ [[package]] name = "gpui_windows" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "anyhow", "collections", @@ -3158,7 +3158,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "anyhow", "async-compression", @@ -3183,7 +3183,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "rustls", "rustls-platform-verifier", @@ -4023,7 +4023,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "anyhow", "bindgen", @@ -4814,7 +4814,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "collections", "serde", @@ -5025,9 +5025,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" dependencies = [ "portable-atomic", ] @@ -5524,7 +5524,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "derive_refineable", ] @@ -5623,7 +5623,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "anyhow", "bytes", @@ -5678,7 +5678,7 @@ dependencies = [ [[package]] name = "rope" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "arrayvec", "log", @@ -5964,7 +5964,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "async-task", "backtrace", @@ -6588,7 +6588,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "arrayvec", "log", @@ -6973,9 +6973,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -7284,9 +7284,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -7348,9 +7348,9 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "uds_windows" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b70b87d15e91f553711b40df3048faf27a7a04e01e0ddc0cf9309f0af7c2ca" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", @@ -7550,7 +7550,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "anyhow", "async-fs", @@ -7589,7 +7589,7 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "perf", "quote", @@ -9391,7 +9391,7 @@ dependencies = [ [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "anyhow", "chrono", @@ -9408,7 +9408,7 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" dependencies = [ "tracing", "tracing-subscriber", @@ -9419,7 +9419,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#7b9afc8c454607222eaf751bbc38159ececc1f7a" +source = "git+https://github.com/zed-industries/zed#cbc39669b414c2601f86ece9faffe164a33b5ad7" [[package]] name = "zune-core" diff --git a/crates/common/src/paths.rs b/crates/common/src/paths.rs index 3017be2..1f23e62 100644 --- a/crates/common/src/paths.rs +++ b/crates/common/src/paths.rs @@ -7,6 +7,13 @@ pub fn home_dir() -> &'static PathBuf { HOME_DIR.get_or_init(|| dirs::home_dir().expect("failed to determine home directory")) } +/// Returns the path to the user's download directory. +pub fn download_dir() -> &'static PathBuf { + static DOWNLOAD_DIR: OnceLock = OnceLock::new(); + DOWNLOAD_DIR + .get_or_init(|| dirs::download_dir().expect("failed to determine download directory")) +} + /// Returns the path to the configuration directory used by Coop. pub fn config_dir() -> &'static PathBuf { static CONFIG_DIR: OnceLock = OnceLock::new(); @@ -56,9 +63,3 @@ pub fn support_dir() -> &'static PathBuf { config_dir().clone() }) } - -/// Returns the path to the `nostr` file. -pub fn nostr_file() -> &'static PathBuf { - static NOSTR_FILE: OnceLock = OnceLock::new(); - NOSTR_FILE.get_or_init(|| support_dir().join("nostr-db")) -} 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 4071dd9..472b01d 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -4,12 +4,14 @@ use std::sync::Arc; use ::settings::AppSettings; use chat::{ChatEvent, ChatRegistry}; +use common::download_dir; use device::{DeviceEvent, DeviceRegistry}; use gpui::prelude::FluentBuilder; 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}; @@ -23,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; @@ -37,6 +40,7 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Workspace::new(window, cx)) } +struct DeviceNotifcation; struct SignerNotifcation; struct RelayNotifcation; @@ -46,9 +50,11 @@ enum Command { ToggleTheme, ToggleAccount, - RefreshEncryption, RefreshRelayList, RefreshMessagingRelays, + BackupEncryption, + ImportEncryption, + RefreshEncryption, ResetEncryption, ShowRelayList, @@ -143,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); @@ -163,13 +169,55 @@ 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() + .id::() + .title("Cannot setup the encryption key") + .message(reason) + .autohide(false) + .with_kind(NotificationKind::Error); + + window.push_notification(note, cx); + } + DeviceEvent::NotSubscribe { reason } => { + let note = Notification::new() + .id::() + .title("Cannot getting messages") + .message(reason) + .autohide(false) + .with_kind(NotificationKind::Error); + + window.push_notification(note, cx); } DeviceEvent::Error(error) => { window.push_notification(Notification::error(error).autohide(false), cx); @@ -404,14 +452,57 @@ impl Workspace { 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")); + + cx.spawn_in(window, async move |_this, cx| { + // Get the output path from the save dialog + let output_path = match save_dialog.await { + Ok(Ok(Some(path))) => path, + Ok(Ok(None)) | Err(_) => return Ok(()), + Ok(Err(error)) => { + cx.update(|window, cx| { + let message = format!("Failed to pick save location: {error:#}"); + let note = Notification::error(message).autohide(false); + window.push_notification(note, cx); + })?; + return Ok(()); + } + }; + + // Get the backup task + let backup = + device.read_with(cx, |this, cx| this.backup(output_path.clone(), cx))?; + + // Run the backup task + backup.await?; + + // Open the backup file with the system's default application + cx.update(|_window, cx| { + cx.open_with_system(output_path.as_path()); + })?; + + Ok::<_, anyhow::Error>(()) + }) + .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() @@ -420,43 +511,31 @@ impl Workspace { .child( div() .italic() - .text_color(cx.theme().warning_active) + .text_color(cx.theme().text_danger) .child(SharedString::from(ENC_WARN)), ), ) - .on_ok(move |_ev, window, cx| { - let device = DeviceRegistry::global(cx); - let task = device.read(cx).create_encryption(cx); - - window - .spawn(cx, async move |cx| { - let result = task.await; - - cx.update(|window, cx| match result { - Ok(keys) => { - device.update(cx, |this, cx| { - this.set_signer(keys, cx); - this.listen_request(cx); - }); - window.close_modal(cx); - } - Err(e) => { - window.push_notification( - Notification::error(e.to_string()).autohide(false), - cx, - ); - } - }) - .ok(); - }) - .detach(); - - // false to keep modal open - false + .on_ok(move |_ev, _window, 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); @@ -466,7 +545,6 @@ impl Workspace { .show_close(false) .keyboard(false) .overlay_closable(false) - .pb_2() .child(accounts.clone()) }); } @@ -479,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![]; @@ -552,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"; @@ -702,27 +779,63 @@ impl Workspace { .ghost() .dropdown_menu(move |this, _window, cx| { let device = DeviceRegistry::global(cx); - let state = device.read(cx).state(); + let subscribing = device.read(cx).subscribing; + let requesting = device.read(cx).requesting; this.min_w(px(260.)) + .label("Encryption Key") + .when(requesting, |this| { + this.item(PopupMenuItem::element(move |_window, cx| { + h_flex() + .px_1() + .w_full() + .gap_2() + .text_sm() + .child( + div() + .size_1p5() + .rounded_full() + .bg(cx.theme().icon_accent), + ) + .child(SharedString::from("Waiting for approval...")) + })) + }) .item(PopupMenuItem::element(move |_window, cx| { h_flex() .px_1() .w_full() .gap_2() .text_sm() - .child( - div() - .size_1p5() - .rounded_full() - .when(state.set(), |this| this.bg(gpui::green())) - .when(state.requesting(), |this| { - this.bg(cx.theme().icon_accent) - }), - ) - .child(SharedString::from(state.to_string())) + .when(!subscribing, |this| { + this.text_color(cx.theme().text_muted) + }) + .child(div().size_1p5().rounded_full().map(|this| { + if subscribing { + this.bg(cx.theme().icon_accent) + } else { + this.bg(cx.theme().icon_muted) + } + })) + .map(|this| { + if subscribing { + this.child("Listening for messages") + } else { + this.child("Idle") + } + }) })) .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, @@ -743,7 +856,7 @@ impl Workspace { .loading(!inbox_connected) .disabled(!inbox_connected) .when(!inbox_connected, |this| { - this.tooltip("Connecting to user's messaging relays...") + this.tooltip("Connecting to the user's messaging relays...") }) .when(inbox_connected, |this| this.indicator()) .dropdown_menu(move |this, _window, cx| { @@ -815,7 +928,7 @@ impl Workspace { .loading(!relay_connected) .disabled(!relay_connected) .when(!relay_connected, |this| { - this.tooltip("Connecting to user's relay list...") + this.tooltip("Connecting to the user's relay list...") }) .when(relay_connected, |this| this.indicator()) .dropdown_menu(move |this, _window, _cx| { diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 9a6fe2d..5120b8d 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -1,5 +1,6 @@ use std::cell::Cell; use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; use std::rc::Rc; use std::time::Duration; @@ -10,9 +11,7 @@ use gpui::{ }; use nostr_sdk::prelude::*; use person::PersonRegistry; -use state::{ - Announcement, DEVICE_GIFTWRAP, DeviceState, NostrRegistry, StateEvent, TIMEOUT, app_name, -}; +use state::{Announcement, DEVICE_GIFTWRAP, NostrRegistry, StateEvent, TIMEOUT, app_name}; use theme::ActiveTheme; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; @@ -36,17 +35,55 @@ impl Global for GlobalDeviceRegistry {} pub enum DeviceEvent { /// A new encryption signer has been set 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 + NotSubscribe { reason: SharedString }, /// An error occurred Error(SharedString), } +impl DeviceEvent { + pub fn error(error: T) -> Self + where + T: Into, + { + Self::Error(error.into()) + } + + pub fn not_subscribe(reason: T) -> Self + where + T: Into, + { + Self::NotSubscribe { + reason: reason.into(), + } + } + + pub fn not_set(reason: T) -> Self + where + T: Into, + { + Self::NotSet { + reason: reason.into(), + } + } +} + /// Device Registry /// /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md #[derive(Debug)] pub struct DeviceRegistry { - /// Device state - state: DeviceState, + /// Whether the registry is currently subscribing to gift wrap events + pub subscribing: bool, + + /// Whether the registry is waiting for encryption key approval from other devices + pub requesting: bool, /// Async tasks tasks: Vec>>, @@ -71,30 +108,30 @@ impl DeviceRegistry { /// Create a new device registry instance fn new(window: &mut Window, cx: &mut Context) -> Self { let nostr = NostrRegistry::global(cx); - let state = DeviceState::default(); - let subscription = Some(cx.subscribe_in( - &nostr, - window, - |this, _state, event, _window, cx| match event { + // Get announcement when signer is set + let subscription = cx.subscribe_in(&nostr, window, |this, _e, event, _window, cx| { + match event { StateEvent::SignerSet => { - this.reset(cx); + this.set_subscribing(false, cx); + this.set_requesting(false, cx); } StateEvent::RelayConnected => { this.get_announcement(cx); } _ => {} - }, - )); + }; + }); cx.defer_in(window, |this, window, cx| { this.handle_notifications(window, cx); }); Self { - state, + subscribing: false, + requesting: false, tasks: vec![], - _subscription: subscription, + _subscription: Some(subscription), } } @@ -140,13 +177,13 @@ impl DeviceRegistry { self.tasks.push(cx.spawn_in(window, async move |this, cx| { while let Ok(event) = rx.recv_async().await { match event.kind { - // New request event + // New request event from other device Kind::Custom(4454) => { this.update_in(cx, |this, window, cx| { this.ask_for_approval(event, window, cx); })?; } - // New response event + // New response event from the master device Kind::Custom(4455) => { this.update(cx, |this, cx| { this.extract_encryption(event, cx); @@ -155,24 +192,24 @@ impl DeviceRegistry { _ => {} } } - Ok(()) })); } - /// Get the device state - pub fn state(&self) -> DeviceState { - self.state.clone() + /// Set whether the registry is currently subscribing to gift wrap events + fn set_subscribing(&mut self, subscribing: bool, cx: &mut Context) { + self.subscribing = subscribing; + cx.notify(); } - /// Set the device state - fn set_state(&mut self, state: DeviceState, cx: &mut Context) { - self.state = state; + /// Set whether the registry is waiting for encryption key approval from other devices + fn set_requesting(&mut self, requesting: bool, cx: &mut Context) { + self.requesting = requesting; cx.notify(); } /// Set the decoupled encryption key for the current user - pub fn set_signer(&mut self, new: S, cx: &mut Context) + fn set_signer(&mut self, new: S, cx: &mut Context) where S: NostrSigner + 'static, { @@ -184,7 +221,7 @@ impl DeviceRegistry { // Update state this.update(cx, |this, cx| { - this.set_state(DeviceState::Set, cx); + cx.emit(DeviceEvent::Set); this.get_messages(cx); })?; @@ -192,12 +229,6 @@ impl DeviceRegistry { })); } - /// Reset the device state - fn reset(&mut self, cx: &mut Context) { - self.state = DeviceState::Idle; - cx.notify(); - } - /// Get all messages for encryption keys fn get_messages(&mut self, cx: &mut Context) { let task = self.subscribe_to_giftwrap_events(cx); @@ -205,59 +236,50 @@ impl DeviceRegistry { self.tasks.push(cx.spawn(async move |this, cx| { if let Err(e) = task.await { this.update(cx, |_this, cx| { - cx.emit(DeviceEvent::Error(SharedString::from(e.to_string()))); + cx.emit(DeviceEvent::not_subscribe(e.to_string())); + })?; + } else { + this.update(cx, |this, cx| { + this.set_subscribing(true, cx); })?; } Ok(()) })); } - /// Get the messaging relays for the current user - fn get_user_messaging_relays(&self, cx: &App) -> Task, Error>> { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - let signer = nostr.read(cx).signer(); - - cx.background_spawn(async move { - let public_key = signer.get_public_key().await?; - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(public_key) - .limit(1); - - if let Some(event) = client.database().query(filter).await?.first_owned() { - // Extract relay URLs from the event - let urls: Vec = nip17::extract_owned_relay_list(event).collect(); - - // Ensure all relays are connected - for url in urls.iter() { - client.add_relay(url).and_connect().await?; - } - - Ok(urls) - } else { - Err(anyhow!("Relays not found")) - } - }) - } - /// Continuously get gift wrap events for the current user in their messaging relays fn subscribe_to_giftwrap_events(&self, cx: &App) -> Task> { + let persons = PersonRegistry::global(cx); let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let signer = nostr.read(cx).signer(); - let urls = self.get_user_messaging_relays(cx); + + let Some(user) = signer.public_key() else { + return Task::ready(Err(anyhow!("User not found"))); + }; + + let profile = persons.read(cx).get(&user, cx); + let relays = profile.messaging_relays().clone(); cx.background_spawn(async move { - let urls = urls.await?; let encryption = signer.get_encryption_signer().await.context("not found")?; let public_key = encryption.get_public_key().await?; let filter = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); let id = SubscriptionId::new(DEVICE_GIFTWRAP); + // Ensure user has relays configured + if relays.is_empty() { + return Err(anyhow!("No messaging relays found")); + } + + // Ensure relays are connected + for url in relays.iter() { + client.add_relay(url).and_connect().await?; + } + // Construct target for subscription - let target: HashMap = urls + let target: HashMap = relays .into_iter() .map(|relay| (relay, filter.clone())) .collect(); @@ -269,6 +291,21 @@ impl DeviceRegistry { }) } + /// Backup the encryption's secret key to a file + pub fn backup(&self, path: PathBuf, cx: &App) -> Task> { + let nostr = NostrRegistry::global(cx); + let client = nostr.read(cx).client(); + + cx.background_spawn(async move { + let keys = get_keys(&client).await?; + let content = keys.secret_key().to_bech32()?; + + smol::fs::write(path, &content).await?; + + Ok(()) + }) + } + /// Get device announcement for current user pub fn get_announcement(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); @@ -302,13 +339,15 @@ impl DeviceRegistry { 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.new_signer(&event, cx); + this.set_encryption(&event, cx); })?; } Err(_) => { + // User has no announcement, create a new one this.update(cx, |this, cx| { - this.announce(cx); + this.set_announcement(Keys::generate(), cx); })?; } } @@ -317,26 +356,55 @@ impl DeviceRegistry { })); } - /// Create new encryption keys - pub fn create_encryption(&self, cx: &App) -> Task> { + /// Create a new device signer and announce it to user's relay list + 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) => { + this.update(cx, |this, cx| { + this.set_signer(keys, cx); + this.wait_for_request(cx); + })?; + } + Err(e) => { + this.update(cx, |_this, cx| { + cx.emit(DeviceEvent::error(e.to_string())); + })?; + } + } + Ok(()) + })); + } + + /// 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(); cx.background_spawn(async move { // Construct an announcement event - let event = client - .sign_event_builder(EventBuilder::new(Kind::Custom(10044), "").tags(vec![ - Tag::custom(TagKind::custom("n"), vec![n]), - Tag::client(app_name()), - ])) - .await?; + let builder = EventBuilder::new(Kind::Custom(10044), "").tags(vec![ + Tag::custom(TagKind::custom("n"), vec![n]), + Tag::client(app_name()), + ]); + + // Sign the event with user's signer + 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?; @@ -345,39 +413,23 @@ impl DeviceRegistry { }) } - /// Create a new device signer and announce it - fn announce(&mut self, cx: &mut Context) { - let task = self.create_encryption(cx); - - self.tasks.push(cx.spawn(async move |this, cx| { - let keys = task.await?; - - // Update signer - this.update(cx, |this, cx| { - this.set_signer(keys, cx); - this.listen_request(cx); - })?; - - Ok(()) - })); - } - - /// Initialize device signer (decoupled encryption key) for the current user - pub fn new_signer(&mut self, event: &Event, cx: &mut Context) { + /// Set encryption key from the announcement event + fn set_encryption(&mut self, event: &Event, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); 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 { if let Ok(keys) = get_keys(&client).await { if keys.public_key() != device_pubkey { - return Err(anyhow!("Key mismatch")); + return Err(anyhow!("Encryption Key doesn't match the announcement")); }; Ok(keys) } else { - Err(anyhow!("Key not found")) + Err(anyhow!("Encryption Key not found. Please create a new key")) } }); @@ -386,74 +438,49 @@ impl DeviceRegistry { Ok(keys) => { this.update(cx, |this, cx| { this.set_signer(keys, cx); - this.listen_request(cx); + this.wait_for_request(cx); })?; } Err(e) => { - log::warn!("Failed to initialize device signer: {e}"); - this.update(cx, |this, cx| { - this.request(cx); - this.listen_approval(cx); + this.update(cx, |_this, cx| { + cx.emit(DeviceEvent::not_set(e.to_string())); })?; } }; - Ok(()) })); } - /// Listen for device key requests on user's write relays - pub fn listen_request(&mut self, cx: &mut Context) { + /// Wait for encryption key requests from now on + 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(public_key) = signer.public_key() else { - return; - }; + self.tasks.push(cx.background_spawn(async move { + let public_key = signer.get_public_key().await?; - let task: Task> = cx.background_spawn(async move { - // Construct a filter for device key requests - let filter = Filter::new() + // Construct a filter for encryption key requests + let now = Filter::new() .kind(Kind::Custom(4454)) .author(public_key) .since(Timestamp::now()); - // Subscribe to the device key requests on user's write relays - client.subscribe(filter).await?; - - Ok(()) - }); - - task.detach(); - } - - /// Listen for device key approvals on user's write relays - fn listen_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(public_key) = signer.public_key() else { - return; - }; - - self.tasks.push(cx.background_spawn(async move { - // Construct a filter for device key requests - let filter = Filter::new() - .kind(Kind::Custom(4455)) + // Construct a filter for the last encryption key request + let last = Filter::new() + .kind(Kind::Custom(4454)) .author(public_key) - .since(Timestamp::now()); + .limit(1); // Subscribe to the device key requests on user's write relays - client.subscribe(filter).await?; + client.subscribe(vec![now, last]).await?; Ok(()) })); } /// Request encryption keys from other device - fn request(&mut self, cx: &mut Context) { + 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(); @@ -461,9 +488,10 @@ impl DeviceRegistry { let app_keys = nostr.read(cx).keys(); let app_pubkey = app_keys.public_key(); - let task: Task, Error>> = cx.background_spawn(async move { + let task: Task, Error>> = cx.background_spawn(async move { let public_key = signer.get_public_key().await?; + // Construct a filter to get the latest approval event let filter = Filter::new() .kind(Kind::Custom(4455)) .author(public_key) @@ -471,30 +499,18 @@ impl DeviceRegistry { .limit(1); match client.database().query(filter).await?.first_owned() { - Some(event) => { - let root_device = event - .tags - .find(TagKind::custom("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(&root_device, payload).await?; - - let secret = SecretKey::from_hex(&decrypted)?; - let keys = Keys::new(secret); - - Ok(Some(keys)) - } + // Found an approval event + Some(event) => Ok(Some(event)), + // No approval event found, construct a request event None => { // Construct an event for device key request - let event = client - .sign_event_builder(EventBuilder::new(Kind::Custom(4454), "").tags(vec![ - Tag::client(app_name()), - Tag::custom(TagKind::custom("P"), vec![app_pubkey]), - ])) - .await?; + let builder = EventBuilder::new(Kind::Custom(4454), "").tags(vec![ + Tag::custom(TagKind::custom("P"), vec![app_pubkey]), + Tag::client(app_name()), + ]); + + // Sign the event with user's signer + let event = client.sign_event_builder(builder).await?; // Send the event to write relays client.send_event(&event).to_nip65().await?; @@ -506,32 +522,58 @@ impl DeviceRegistry { self.tasks.push(cx.spawn(async move |this, cx| { match task.await { - Ok(Some(keys)) => { + Ok(Some(event)) => { this.update(cx, |this, cx| { - this.set_signer(keys, cx); + this.extract_encryption(event, cx); })?; } Ok(None) => { this.update(cx, |this, cx| { - this.set_state(DeviceState::Requesting, cx); + this.set_requesting(true, cx); + this.wait_for_approval(cx); + + cx.emit(DeviceEvent::Requesting); })?; } Err(e) => { - log::error!("Failed to request the encryption key: {e}"); + this.update(cx, |_this, cx| { + cx.emit(DeviceEvent::error(e.to_string())); + })?; } }; + Ok(()) + })); + } + + /// Wait for encryption key approvals + 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(); + + self.tasks.push(cx.background_spawn(async move { + let public_key = signer.get_public_key().await?; + + // Construct a filter for device key requests + let filter = Filter::new() + .kind(Kind::Custom(4455)) + .author(public_key) + .since(Timestamp::now()); + + // Subscribe to the device key requests on user's write relays + client.subscribe(filter).await?; Ok(()) })); } - /// Parse the response event for device keys from other devices + /// 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 task: Task> = cx.background_spawn(async move { - let root_device = event + let master = event .tags .find(TagKind::custom("P")) .and_then(|tag| tag.content()) @@ -539,7 +581,7 @@ impl DeviceRegistry { .context("Invalid event's tags")?; let payload = event.content.as_str(); - let decrypted = app_keys.nip44_decrypt(&root_device, payload).await?; + let decrypted = app_keys.nip44_decrypt(&master, payload).await?; let secret = SecretKey::from_hex(&decrypted)?; let keys = Keys::new(secret); @@ -548,13 +590,19 @@ impl DeviceRegistry { }); self.tasks.push(cx.spawn(async move |this, cx| { - let keys = task.await?; - - // Update signer - this.update(cx, |this, cx| { - this.set_signer(keys, cx); - })?; - + match task.await { + Ok(keys) => { + this.update(cx, |this, cx| { + this.set_signer(keys, cx); + this.set_requesting(false, cx); + })?; + } + Err(e) => { + this.update(cx, |_this, cx| { + cx.emit(DeviceEvent::not_set(e.to_string())); + })?; + } + } Ok(()) })); } diff --git a/crates/state/src/device.rs b/crates/state/src/device.rs index 11b6a3c..4aae264 100644 --- a/crates/state/src/device.rs +++ b/crates/state/src/device.rs @@ -1,40 +1,6 @@ -use std::fmt::Display; - use gpui::SharedString; use nostr_sdk::prelude::*; -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] -pub enum DeviceState { - #[default] - Idle, - Requesting, - Set, -} - -impl Display for DeviceState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DeviceState::Idle => write!(f, "Idle"), - DeviceState::Requesting => write!(f, "Wait for approval"), - DeviceState::Set => write!(f, "Encryption Key is ready"), - } - } -} - -impl DeviceState { - pub fn idle(&self) -> bool { - matches!(self, DeviceState::Idle) - } - - pub fn requesting(&self) -> bool { - matches!(self, DeviceState::Requesting) - } - - pub fn set(&self) -> bool { - matches!(self, DeviceState::Set) - } -} - /// Announcement #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Announcement {