From 10ded51d2f428754f7accf8bfa308b5532ffab0f Mon Sep 17 00:00:00 2001 From: reya Date: Tue, 24 Feb 2026 15:48:58 +0700 Subject: [PATCH] update encryption panel --- assets/icons/reset.svg | 3 + crates/common/src/display.rs | 42 ---- crates/coop/src/dialogs/screening.rs | 4 +- crates/coop/src/panels/encryption_key.rs | 264 ++++++++++++++++++++- crates/coop/src/panels/messaging_relays.rs | 1 + crates/coop/src/panels/profile.rs | 3 +- crates/coop/src/panels/relay_list.rs | 1 + crates/coop/src/workspace.rs | 21 +- crates/device/src/lib.rs | 123 +++++----- crates/state/src/constants.rs | 2 +- crates/state/src/device.rs | 14 ++ crates/ui/src/icon.rs | 2 + 12 files changed, 352 insertions(+), 128 deletions(-) create mode 100644 assets/icons/reset.svg diff --git a/assets/icons/reset.svg b/assets/icons/reset.svg new file mode 100644 index 0000000..4bc319d --- /dev/null +++ b/assets/icons/reset.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/common/src/display.rs b/crates/common/src/display.rs index 3f967ff..7b39ae0 100644 --- a/crates/common/src/display.rs +++ b/crates/common/src/display.rs @@ -13,38 +13,6 @@ const MINUTES_IN_HOUR: i64 = 60; const HOURS_IN_DAY: i64 = 24; const DAYS_IN_MONTH: i64 = 30; -pub trait RenderedProfile { - fn avatar(&self) -> SharedString; - fn display_name(&self) -> SharedString; -} - -impl RenderedProfile for Profile { - fn avatar(&self) -> SharedString { - self.metadata() - .picture - .as_ref() - .filter(|picture| !picture.is_empty()) - .map(|picture| picture.into()) - .unwrap_or_else(|| "brand/avatar.png".into()) - } - - fn display_name(&self) -> SharedString { - if let Some(display_name) = self.metadata().display_name.as_ref() { - if !display_name.is_empty() { - return SharedString::from(display_name); - } - } - - if let Some(name) = self.metadata().name.as_ref() { - if !name.is_empty() { - return SharedString::from(name); - } - } - - SharedString::from(shorten_pubkey(self.public_key(), 4)) - } -} - pub trait RenderedTimestamp { fn to_human_time(&self) -> SharedString; fn to_ago(&self) -> SharedString; @@ -126,13 +94,3 @@ impl> TextUtils for T { ))) } } - -pub fn shorten_pubkey(public_key: PublicKey, len: usize) -> String { - let Ok(pubkey) = public_key.to_bech32(); - - format!( - "{}:{}", - &pubkey[0..(len + 1)], - &pubkey[pubkey.len() - len..] - ) -} diff --git a/crates/coop/src/dialogs/screening.rs b/crates/coop/src/dialogs/screening.rs index 5cfb8e8..d7ff40e 100644 --- a/crates/coop/src/dialogs/screening.rs +++ b/crates/coop/src/dialogs/screening.rs @@ -2,14 +2,14 @@ use std::collections::HashMap; use std::time::Duration; use anyhow::{Context as AnyhowContext, Error}; -use common::{shorten_pubkey, RenderedTimestamp}; +use common::RenderedTimestamp; use gpui::prelude::FluentBuilder; use gpui::{ div, px, relative, rems, uniform_list, App, AppContext, Context, Div, Entity, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, }; use nostr_sdk::prelude::*; -use person::{Person, PersonRegistry}; +use person::{shorten_pubkey, Person, PersonRegistry}; use smallvec::{smallvec, SmallVec}; use state::{NostrAddress, NostrRegistry, BOOTSTRAP_RELAYS, TIMEOUT}; use theme::ActiveTheme; diff --git a/crates/coop/src/panels/encryption_key.rs b/crates/coop/src/panels/encryption_key.rs index 0f243ac..4584a63 100644 --- a/crates/coop/src/panels/encryption_key.rs +++ b/crates/coop/src/panels/encryption_key.rs @@ -1,27 +1,151 @@ +use anyhow::Error; +use device::DeviceRegistry; +use gpui::prelude::FluentBuilder; use gpui::{ - AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, - IntoElement, Render, SharedString, Styled, Window, + div, px, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, + IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, }; +use nostr_sdk::prelude::*; +use person::{shorten_pubkey, PersonRegistry}; +use state::Announcement; +use theme::ActiveTheme; +use ui::button::{Button, ButtonVariants}; use ui::dock_area::panel::{Panel, PanelEvent}; -use ui::v_flex; +use ui::notification::Notification; +use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension}; -pub fn init(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| EncryptionPanel::new(window, cx)) +const 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 NOTICE: &str = "By resetting your encryption key, you will lose access to \ + all your encrypted messages before. This action cannot be undone."; + +pub fn init(public_key: PublicKey, window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| EncryptionPanel::new(public_key, window, cx)) } #[derive(Debug)] pub struct EncryptionPanel { name: SharedString, focus_handle: FocusHandle, + + /// User's public key + public_key: PublicKey, + + /// Whether the panel is loading + loading: bool, + + /// Tasks + tasks: Vec>>, } impl EncryptionPanel { - fn new(_window: &mut Window, cx: &mut Context) -> Self { + fn new(public_key: PublicKey, _window: &mut Window, cx: &mut Context) -> Self { Self { name: "Encryption".into(), focus_handle: cx.focus_handle(), + public_key, + loading: false, + tasks: vec![], } } + + fn set_loading(&mut self, status: bool, cx: &mut Context) { + self.loading = status; + cx.notify(); + } + + fn approve(&mut self, event: &Event, window: &mut Window, cx: &mut Context) { + let device = DeviceRegistry::global(cx); + let task = device.read(cx).approve(event, cx); + let id = event.id; + + // Update loading status + self.set_loading(true, cx); + + self.tasks.push(cx.spawn_in(window, async move |this, cx| { + match task.await { + Ok(_) => { + this.update_in(cx, |this, window, cx| { + // Reset loading status + this.set_loading(false, cx); + + // Remove request + device.update(cx, |this, cx| { + this.remove_request(&id, cx); + }); + + window.push_notification("Approved", cx); + })?; + } + Err(e) => { + this.update_in(cx, |this, window, cx| { + this.set_loading(false, cx); + window.push_notification(Notification::error(e.to_string()), cx); + })?; + } + } + + Ok(()) + })); + } + + fn render_requests(&mut self, cx: &mut Context) -> Vec { + const TITLE: &str = "You've requested for the Encryption Key from:"; + + let device = DeviceRegistry::global(cx); + let requests = device.read(cx).requests.clone(); + let mut items = Vec::new(); + + for event in requests.into_iter() { + let request = Announcement::from(&event); + let client_name = request.client_name(); + let target = request.public_key(); + + items.push( + v_flex() + .gap_2() + .text_sm() + .child(SharedString::from(TITLE)) + .child( + v_flex() + .h_12() + .items_center() + .justify_center() + .px_2() + .rounded(cx.theme().radius) + .bg(cx.theme().warning_background) + .text_color(cx.theme().warning_foreground) + .child(client_name.clone()), + ) + .child( + h_flex() + .h_7() + .w_full() + .px_2() + .rounded(cx.theme().radius) + .bg(cx.theme().elevated_surface_background) + .child(SharedString::from(target.to_hex())), + ) + .child( + h_flex().justify_end().gap_2().child( + Button::new("approve") + .label("Approve") + .ghost() + .small() + .disabled(self.loading) + .loading(self.loading) + .on_click(cx.listener(move |this, _ev, window, cx| { + this.approve(&event, window, cx); + })), + ), + ), + ) + } + + items + } } impl Panel for EncryptionPanel { @@ -43,12 +167,128 @@ impl Focusable for EncryptionPanel { } impl Render for EncryptionPanel { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let device = DeviceRegistry::global(cx); + let state = device.read(cx).state(); + let has_requests = device.read(cx).has_requests(); + + let persons = PersonRegistry::global(cx); + let profile = persons.read(cx).get(&self.public_key, cx); + + let Some(announcement) = profile.announcement() else { + return div(); + }; + + let pubkey = SharedString::from(shorten_pubkey(announcement.public_key(), 16)); + let client_name = announcement.client_name(); + v_flex() - .size_full() - .items_center() - .justify_center() - .p_2() - .gap_10() + .p_3() + .gap_3() + .w_full() + .child( + div() + .text_xs() + .text_color(cx.theme().text_muted) + .child(SharedString::from(MSG)), + ) + .child(divider(cx)) + .child( + v_flex() + .gap_3() + .text_sm() + .child( + v_flex() + .gap_1p5() + .child( + div() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Device Name:")), + ) + .child( + h_flex() + .h_12() + .items_center() + .justify_center() + .rounded(cx.theme().radius) + .bg(cx.theme().elevated_surface_background) + .child(client_name.clone()), + ), + ) + .child( + v_flex() + .gap_1p5() + .child( + div() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Encryption Public Key:")), + ) + .child( + h_flex() + .h_7() + .w_full() + .px_2() + .rounded(cx.theme().radius) + .bg(cx.theme().elevated_surface_background) + .child(pubkey), + ), + ), + ) + .when(has_requests, |this| { + this.child(divider(cx)).child( + v_flex() + .gap_1p5() + .w_full() + .child( + div() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Requests:")), + ) + .child( + v_flex() + .gap_2() + .flex_1() + .w_full() + .children(self.render_requests(cx)), + ), + ) + }) + .child(divider(cx)) + .when(state.requesting(), |this| { + this.child( + h_flex() + .h_8() + .justify_center() + .text_xs() + .text_center() + .text_color(cx.theme().text_accent) + .bg(cx.theme().elevated_surface_background) + .rounded(cx.theme().radius) + .child(SharedString::from( + "Please open other device and approve the request", + )), + ) + }) + .when(state.set(), |this| { + this.child( + v_flex() + .gap_1() + .child( + Button::new("reset") + .icon(IconName::Reset) + .label("Reset") + .warning() + .small() + .font_semibold(), + ) + .child( + div() + .italic() + .text_size(px(10.)) + .text_color(cx.theme().text_muted) + .child(SharedString::from(NOTICE)), + ), + ) + }) } } diff --git a/crates/coop/src/panels/messaging_relays.rs b/crates/coop/src/panels/messaging_relays.rs index 979ce17..4c0dd00 100644 --- a/crates/coop/src/panels/messaging_relays.rs +++ b/crates/coop/src/panels/messaging_relays.rs @@ -214,6 +214,7 @@ impl MessagingRelayPanel { } Err(e) => { this.update_in(cx, |this, window, cx| { + this.set_updating(false, cx); this.set_error(e.to_string(), window, cx); })?; } diff --git a/crates/coop/src/panels/profile.rs b/crates/coop/src/panels/profile.rs index b0817e7..5a13b98 100644 --- a/crates/coop/src/panels/profile.rs +++ b/crates/coop/src/panels/profile.rs @@ -2,7 +2,6 @@ use std::str::FromStr; use std::time::Duration; use anyhow::{anyhow, Error}; -use common::shorten_pubkey; use gpui::{ div, rems, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, PathPromptOptions, Render, SharedString, @@ -10,7 +9,7 @@ use gpui::{ }; use gpui_tokio::Tokio; use nostr_sdk::prelude::*; -use person::{Person, PersonRegistry}; +use person::{shorten_pubkey, Person, PersonRegistry}; use settings::AppSettings; use smol::fs; use state::{nostr_upload, NostrRegistry}; diff --git a/crates/coop/src/panels/relay_list.rs b/crates/coop/src/panels/relay_list.rs index 3207dad..55d576d 100644 --- a/crates/coop/src/panels/relay_list.rs +++ b/crates/coop/src/panels/relay_list.rs @@ -234,6 +234,7 @@ impl RelayListPanel { } Err(e) => { this.update_in(cx, |this, window, cx| { + this.set_updating(false, cx); this.set_error(e.to_string(), window, cx); })?; } diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs index 6554137..1cf1830 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -182,14 +182,19 @@ impl Workspace { fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context) { match command { Command::OpenEncryptionPanel => { - self.dock.update(cx, |this, cx| { - this.add_panel( - Arc::new(encryption_key::init(window, cx)), - DockPlacement::Right, - window, - cx, - ); - }); + let nostr = NostrRegistry::global(cx); + let signer = nostr.read(cx).signer(); + + if let Some(public_key) = signer.public_key() { + self.dock.update(cx, |this, cx| { + this.add_panel( + Arc::new(encryption_key::init(public_key, window, cx)), + DockPlacement::Right, + window, + cx, + ); + }); + } } Command::OpenInboxPanel => { self.dock.update(cx, |this, cx| { diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index f711950..6ea6d2f 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -25,12 +25,12 @@ impl Global for GlobalDeviceRegistry {} /// NIP-4e: https://github.com/nostr-protocol/nips/blob/per-device-keys/4e.md #[derive(Debug)] pub struct DeviceRegistry { + /// Request for encryption key from other devices + pub requests: Vec, + /// Device state state: DeviceState, - /// Device requests - requests: Entity>, - /// Async tasks tasks: Vec>>, @@ -52,8 +52,6 @@ impl DeviceRegistry { /// Create a new device registry instance fn new(window: &mut Window, cx: &mut Context) -> Self { let nostr = NostrRegistry::global(cx); - let requests = cx.new(|_| HashSet::default()); - let mut subscriptions = smallvec![]; subscriptions.push( @@ -77,7 +75,7 @@ impl DeviceRegistry { }); Self { - requests, + requests: vec![], state: DeviceState::default(), tasks: vec![], _subscriptions: subscriptions, @@ -89,7 +87,7 @@ impl DeviceRegistry { let client = nostr.read(cx).client(); let (tx, rx) = flume::bounded::(100); - cx.background_spawn(async move { + self.tasks.push(cx.background_spawn(async move { let mut notifications = client.notifications(); let mut processed_events = HashSet::new(); @@ -107,20 +105,21 @@ impl DeviceRegistry { match event.kind { Kind::Custom(4454) => { if verify_author(&client, event.as_ref()).await { - tx.send_async(event.into_owned()).await.ok(); + tx.send_async(event.into_owned()).await?; } } Kind::Custom(4455) => { if verify_author(&client, event.as_ref()).await { - tx.send_async(event.into_owned()).await.ok(); + tx.send_async(event.into_owned()).await?; } } _ => {} } } } - }) - .detach(); + + Ok(()) + })); self.tasks.push( // Update GPUI states @@ -147,8 +146,8 @@ impl DeviceRegistry { } /// Get the device state - pub fn state(&self) -> &DeviceState { - &self.state + pub fn state(&self) -> DeviceState { + self.state.clone() } /// Set the device state @@ -181,19 +180,25 @@ impl DeviceRegistry { /// Reset the device state fn reset(&mut self, cx: &mut Context) { self.state = DeviceState::Idle; - self.requests.update(cx, |this, cx| { - this.clear(); - cx.notify(); - }); + self.requests.clear(); cx.notify(); } /// Add a request for device keys fn add_request(&mut self, request: Event, cx: &mut Context) { - self.requests.update(cx, |this, cx| { - this.insert(request); - cx.notify(); - }); + self.requests.push(request); + cx.notify(); + } + + /// Remove a request for device keys + pub fn remove_request(&mut self, id: &EventId, cx: &mut Context) { + self.requests.retain(|r| r.id != *id); + cx.notify(); + } + + /// Check if there are any pending requests + pub fn has_requests(&self) -> bool { + !self.requests.is_empty() } /// Get all messages for encryption keys @@ -290,12 +295,12 @@ impl DeviceRegistry { match task.await { Ok(event) => { this.update(cx, |this, cx| { - this.init_device_signer(&event, cx); + this.new_signer(&event, cx); })?; } Err(_) => { this.update(cx, |this, cx| { - this.announce_device(cx); + this.announce(cx); })?; } } @@ -305,13 +310,15 @@ impl DeviceRegistry { } /// Create a new device signer and announce it - fn announce_device(&mut self, cx: &mut Context) { + fn announce(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + // Get current user let signer = nostr.read(cx).signer(); let public_key = signer.public_key().unwrap(); + // Get user's write relays let write_relays = nostr.read(cx).write_relays(&public_key, cx); let keys = Keys::generate(); @@ -338,20 +345,20 @@ impl DeviceRegistry { Ok(()) }); - cx.spawn(async move |this, cx| { + self.tasks.push(cx.spawn(async move |this, cx| { if task.await.is_ok() { this.update(cx, |this, cx| { this.set_signer(keys, cx); - this.listen_device_request(cx); - }) - .ok(); + this.listen_request(cx); + })?; } - }) - .detach(); + + Ok(()) + })); } /// Initialize device signer (decoupled encryption key) for the current user - fn init_device_signer(&mut self, event: &Event, cx: &mut Context) { + fn new_signer(&mut self, event: &Event, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); @@ -375,14 +382,14 @@ impl DeviceRegistry { Ok(keys) => { this.update(cx, |this, cx| { this.set_signer(keys, cx); - this.listen_device_request(cx); + this.listen_request(cx); }) .ok(); } Err(e) => { this.update(cx, |this, cx| { - this.request_device_keys(cx); - this.listen_device_approval(cx); + this.request(cx); + this.listen_approval(cx); }) .ok(); @@ -394,7 +401,7 @@ impl DeviceRegistry { } /// Listen for device key requests on user's write relays - fn listen_device_request(&mut self, cx: &mut Context) { + fn listen_request(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); @@ -426,7 +433,7 @@ impl DeviceRegistry { } /// Listen for device key approvals on user's write relays - fn listen_device_approval(&mut self, cx: &mut Context) { + fn listen_approval(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); @@ -435,7 +442,7 @@ impl DeviceRegistry { let write_relays = nostr.read(cx).write_relays(&public_key, cx); - let task: Task> = cx.background_spawn(async move { + self.tasks.push(cx.background_spawn(async move { let urls = write_relays.await; // Construct a filter for device key requests @@ -452,13 +459,11 @@ impl DeviceRegistry { client.subscribe(target).await?; Ok(()) - }); - - task.detach(); + })); } /// Request encryption keys from other device - fn request_device_keys(&mut self, cx: &mut Context) { + fn request(&mut self, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); @@ -559,34 +564,32 @@ impl DeviceRegistry { Ok(keys) }); - cx.spawn(async move |this, cx| { - match task.await { - Ok(keys) => { - this.update(cx, |this, cx| { - this.set_signer(keys, cx); - }) - .ok(); - } - Err(e) => { - log::error!("Error: {e}") - } - }; - }) - .detach(); + 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); + })?; + + Ok(()) + })); } /// Approve requests for device keys from other devices - #[allow(dead_code)] - fn approve(&mut self, event: Event, cx: &mut Context) { + pub fn approve(&self, event: &Event, cx: &App) -> Task> { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); + // Get current user let signer = nostr.read(cx).signer(); let public_key = signer.public_key().unwrap(); + // Get user's write relays let write_relays = nostr.read(cx).write_relays(&public_key, cx); + let event = event.clone(); - let task: Task> = cx.background_spawn(async move { + cx.background_spawn(async move { let urls = write_relays.await; // Get device keys @@ -619,9 +622,7 @@ impl DeviceRegistry { client.send_event(&event).to(urls).await?; Ok(()) - }); - - task.detach(); + }) } } diff --git a/crates/state/src/constants.rs b/crates/state/src/constants.rs index 13585e1..cb34e2b 100644 --- a/crates/state/src/constants.rs +++ b/crates/state/src/constants.rs @@ -4,7 +4,7 @@ use std::sync::OnceLock; pub const CLIENT_NAME: &str = "Coop"; /// COOP's public key -pub const COOP_PUBKEY: &str = "npub126kl5fruqan90py77gf6pvfvygefl2mu2ukew6xdx5pc5uqscwgsnkgarv"; +pub const COOP_PUBKEY: &str = "npub1j3rz3ndl902lya6ywxvy5c983lxs8mpukqnx4pa4lt5wrykwl5ys7wpw3x"; /// App ID pub const APP_ID: &str = "su.reya.coop"; diff --git a/crates/state/src/device.rs b/crates/state/src/device.rs index 93dfd38..682e8b2 100644 --- a/crates/state/src/device.rs +++ b/crates/state/src/device.rs @@ -9,6 +9,20 @@ pub enum DeviceState { Set, } +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 { diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index fa79d0b..c596bd0 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -47,6 +47,7 @@ pub enum IconName { Plus, PlusCircle, Profile, + Reset, Relay, Reply, Refresh, @@ -112,6 +113,7 @@ impl IconNamed for IconName { Self::Plus => "icons/plus.svg", Self::PlusCircle => "icons/plus-circle.svg", Self::Profile => "icons/profile.svg", + Self::Reset => "icons/reset.svg", Self::Relay => "icons/relay.svg", Self::Reply => "icons/reply.svg", Self::Refresh => "icons/refresh.svg",