diff --git a/Cargo.lock b/Cargo.lock index 38419e7..a5e8e7c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1668,6 +1668,8 @@ dependencies = [ "smallvec", "smol", "state", + "theme", + "ui", ] [[package]] diff --git a/crates/coop/src/panels/encryption_key.rs b/crates/coop/src/panels/encryption_key.rs deleted file mode 100644 index 9e0920e..0000000 --- a/crates/coop/src/panels/encryption_key.rs +++ /dev/null @@ -1,292 +0,0 @@ -use anyhow::Error; -use device::DeviceRegistry; -use gpui::prelude::FluentBuilder; -use gpui::{ - 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::notification::Notification; -use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable, StyledExt, WindowExtension}; - -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(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 { - fn panel_id(&self) -> SharedString { - self.name.clone() - } - - fn title(&self, _cx: &App) -> AnyElement { - self.name.clone().into_any_element() - } -} - -impl EventEmitter for EncryptionPanel {} - -impl Focusable for EncryptionPanel { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for EncryptionPanel { - 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() - .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", - )), - ) - }) - .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/mod.rs b/crates/coop/src/panels/mod.rs index a8c04d2..c1425d4 100644 --- a/crates/coop/src/panels/mod.rs +++ b/crates/coop/src/panels/mod.rs @@ -1,7 +1,6 @@ pub mod backup; pub mod connect; pub mod contact_list; -pub mod encryption_key; pub mod greeter; pub mod import; pub mod messaging_relays; diff --git a/crates/coop/src/workspace.rs b/crates/coop/src/workspace.rs index f85a471..bf0e0f9 100644 --- a/crates/coop/src/workspace.rs +++ b/crates/coop/src/workspace.rs @@ -24,9 +24,7 @@ use ui::notification::Notification; use ui::{h_flex, v_flex, IconName, Root, Sizable, WindowExtension}; use crate::dialogs::settings; -use crate::panels::{ - backup, contact_list, encryption_key, greeter, messaging_relays, profile, relay_list, -}; +use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list}; use crate::sidebar; const ENC_MSG: &str = @@ -52,7 +50,6 @@ enum Command { ShowRelayList, ShowMessaging, - ShowEncryption, ShowProfile, ShowSettings, ShowBackup, @@ -249,21 +246,6 @@ impl Workspace { ); }); } - Command::ShowEncryption => { - 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::ShowMessaging => { self.dock.update(cx, |this, cx| { this.add_panel( @@ -544,28 +526,23 @@ impl Workspace { let state = device.read(cx).state(); this.min_w(px(260.)) - .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(gpui::yellow()) - }), - ) - .child(SharedString::from(state.to_string())) - }) - .on_click(|_ev, window, cx| { - window.dispatch_action(Box::new(Command::ShowEncryption), cx); - }), - ) + .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(gpui::yellow()) + }), + ) + .child(SharedString::from(state.to_string())) + })) .separator() .menu_with_icon( "Reload", diff --git a/crates/device/Cargo.toml b/crates/device/Cargo.toml index 051b838..5b73a5e 100644 --- a/crates/device/Cargo.toml +++ b/crates/device/Cargo.toml @@ -8,6 +8,8 @@ publish.workspace = true common = { path = "../common" } state = { path = "../state" } person = { path = "../person" } +ui = { path = "../ui" } +theme = { path = "../theme" } gpui.workspace = true nostr-sdk.workspace = true diff --git a/crates/device/src/lib.rs b/crates/device/src/lib.rs index 050a910..2d84fb5 100644 --- a/crates/device/src/lib.rs +++ b/crates/device/src/lib.rs @@ -1,16 +1,28 @@ +use std::cell::Cell; use std::collections::{HashMap, HashSet}; +use std::rc::Rc; use std::time::Duration; use anyhow::{anyhow, Context as AnyhowContext, Error}; -use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window}; +use gpui::{ + div, App, AppContext, Context, Entity, Global, IntoElement, ParentElement, SharedString, + Styled, Subscription, Task, Window, +}; use nostr_sdk::prelude::*; use person::PersonRegistry; use smallvec::{smallvec, SmallVec}; use state::{ app_name, Announcement, DeviceState, NostrRegistry, RelayState, DEVICE_GIFTWRAP, TIMEOUT, }; +use theme::ActiveTheme; +use ui::avatar::Avatar; +use ui::button::{Button, ButtonVariants}; +use ui::notification::Notification; +use ui::{h_flex, v_flex, Disableable, IconName, Sizable, WindowExtension}; 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); @@ -25,9 +37,6 @@ 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, @@ -64,19 +73,18 @@ impl DeviceRegistry { ); // Run at the end of current cycle - cx.defer_in(window, |this, _window, cx| { - this.handle_notifications(cx); + cx.defer_in(window, |this, window, cx| { + this.handle_notifications(window, cx); }); Self { - requests: vec![], state: DeviceState::default(), tasks: vec![], _subscriptions: subscriptions, } } - fn handle_notifications(&mut self, cx: &mut Context) { + fn handle_notifications(&mut self, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); let (tx, rx) = flume::bounded::(100); @@ -117,17 +125,19 @@ impl DeviceRegistry { self.tasks.push( // Update GPUI states - cx.spawn(async move |this, cx| { + cx.spawn_in(window, async move |this, cx| { while let Ok(event) = rx.recv_async().await { match event.kind { + // New request event Kind::Custom(4454) => { - this.update(cx, |this, cx| { - this.add_request(event, cx); + this.update_in(cx, |this, window, cx| { + this.ask_for_approval(event, window, cx); })?; } + // New response event Kind::Custom(4455) => { this.update(cx, |this, cx| { - this.parse_response(event, cx); + this.extract_encryption(event, cx); })?; } _ => {} @@ -174,27 +184,9 @@ impl DeviceRegistry { /// Reset the device state fn reset(&mut self, cx: &mut Context) { self.state = DeviceState::Idle; - self.requests.clear(); cx.notify(); } - /// Add a request for device keys - fn add_request(&mut self, request: Event, cx: &mut Context) { - 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 fn get_messages(&mut self, cx: &mut Context) { let task = self.subscribe_to_giftwrap_events(cx); @@ -522,30 +514,29 @@ impl DeviceRegistry { } }); - cx.spawn(async move |this, cx| { + self.tasks.push(cx.spawn(async move |this, cx| { match task.await { Ok(Some(keys)) => { this.update(cx, |this, cx| { this.set_signer(keys, cx); - }) - .ok(); + })?; } Ok(None) => { this.update(cx, |this, cx| { this.set_state(DeviceState::Requesting, cx); - }) - .ok(); + })?; } Err(e) => { log::error!("Failed to request the encryption key: {e}"); } }; - }) - .detach(); + + Ok(()) + })); } /// Parse the response event for device keys from other devices - fn parse_response(&mut self, event: Event, cx: &mut Context) { + fn extract_encryption(&mut self, event: Event, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let app_keys = nostr.read(cx).app_keys().clone(); @@ -579,7 +570,7 @@ impl DeviceRegistry { } /// Approve requests for device keys from other devices - pub fn approve(&self, event: &Event, cx: &App) -> Task> { + fn approve(&mut self, event: &Event, window: &mut Window, cx: &mut Context) { let nostr = NostrRegistry::global(cx); let client = nostr.read(cx).client(); @@ -590,8 +581,9 @@ impl DeviceRegistry { // Get user's write relays let write_relays = nostr.read(cx).write_relays(&public_key, cx); let event = event.clone(); + let id: SharedString = event.id.to_hex().into(); - cx.background_spawn(async move { + let task: Task> = cx.background_spawn(async move { let urls = write_relays.await; // Get device keys @@ -613,18 +605,145 @@ impl DeviceRegistry { // // P tag: the current device's public key // p tag: the requester's public key - let event = client - .sign_event_builder(EventBuilder::new(Kind::Custom(4455), payload).tags(vec![ - Tag::custom(TagKind::custom("P"), vec![keys.public_key()]), - Tag::public_key(target), - ])) - .await?; + let builder = EventBuilder::new(Kind::Custom(4455), payload).tags(vec![ + Tag::custom(TagKind::custom("P"), vec![keys.public_key()]), + Tag::public_key(target), + ]); + + // Sign the builder + let event = client.sign_event_builder(builder).await?; // Send the response event to the user's relay list client.send_event(&event).to(urls).await?; Ok(()) + }); + + cx.spawn_in(window, async move |_this, cx| { + match task.await { + Ok(_) => { + cx.update(|window, cx| { + window.clear_notification(id, cx); + }) + .ok(); + } + Err(e) => { + cx.update(|window, cx| { + window.push_notification(Notification::error(e.to_string()), cx); + }) + .ok(); + } + }; }) + .detach(); + } + + /// Handle encryption request + fn ask_for_approval(&mut self, event: Event, window: &mut Window, cx: &mut Context) { + let notification = self.notification(event, cx); + + cx.spawn_in(window, async move |_this, cx| { + cx.update(|window, cx| { + window.push_notification(notification, cx); + }) + .ok(); + }) + .detach(); + } + + /// Build a notification for the encryption request. + fn notification(&self, event: Event, cx: &Context) -> Notification { + let request = Announcement::from(&event); + let persons = PersonRegistry::global(cx); + let profile = persons.read(cx).get(&request.public_key(), cx); + + let entity = cx.entity().downgrade(); + let loading = Rc::new(Cell::new(false)); + + Notification::new() + .custom_id(SharedString::from(event.id.to_hex())) + .autohide(false) + .icon(IconName::UserKey) + .title(SharedString::from("New request")) + .content(move |_window, cx| { + v_flex() + .gap_2() + .text_sm() + .child(SharedString::from(MSG)) + .child( + v_flex() + .gap_2() + .child( + v_flex() + .gap_1() + .text_sm() + .child( + div() + .text_xs() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Requester:")), + ) + .child( + div() + .h_7() + .w_full() + .px_2() + .rounded(cx.theme().radius) + .bg(cx.theme().elevated_surface_background) + .child( + h_flex() + .gap_2() + .child(Avatar::new(profile.avatar()).xsmall()) + .child(profile.name()), + ), + ), + ) + .child( + v_flex() + .gap_1() + .text_sm() + .child( + div() + .text_xs() + .text_color(cx.theme().text_muted) + .child(SharedString::from("Client:")), + ) + .child( + div() + .h_7() + .w_full() + .px_2() + .rounded(cx.theme().radius) + .bg(cx.theme().elevated_surface_background) + .child(request.client_name()), + ), + ), + ) + .into_any_element() + }) + .action(move |_window, _cx| { + let view = entity.clone(); + let event = event.clone(); + + Button::new("approve") + .label("Approve") + .small() + .primary() + .loading(loading.get()) + .disabled(loading.get()) + .on_click({ + let loading = Rc::clone(&loading); + move |_ev, window, cx| { + // Set loading state to true + loading.set(true); + // Process to approve the request + view.update(cx, |this, cx| { + this.approve(&event, window, cx); + }) + .ok(); + } + }) + }) } } diff --git a/crates/relay_auth/src/lib.rs b/crates/relay_auth/src/lib.rs index 96770c1..3a30b3a 100644 --- a/crates/relay_auth/src/lib.rs +++ b/crates/relay_auth/src/lib.rs @@ -343,8 +343,8 @@ impl RelayAuth { .px_1p5() .rounded_sm() .text_xs() - .bg(cx.theme().warning_background) - .text_color(cx.theme().warning_foreground) + .bg(cx.theme().elevated_surface_background) + .text_color(cx.theme().text_accent) .child(url.clone()), ) .into_any_element() @@ -361,11 +361,9 @@ impl RelayAuth { .disabled(loading.get()) .on_click({ let loading = Rc::clone(&loading); - move |_ev, window, cx| { // Set loading state to true loading.set(true); - // Process to approve the request view.update(cx, |this, cx| { this.response(&req, window, cx);