diff --git a/crates/workspace/src/sidebar/header.rs b/crates/workspace/src/sidebar/header.rs index 470d1c81..d90fd917 100644 --- a/crates/workspace/src/sidebar/header.rs +++ b/crates/workspace/src/sidebar/header.rs @@ -1,18 +1,25 @@ #[cfg(not(target_arch = "wasm32"))] use auto_update::AutoUpdater; use gpui::prelude::FluentBuilder; +use gpui::{ + Anchor, App, Context, Div, InteractiveElement, ParentElement, SharedString, Stateful, Styled, + Window, div, px, +}; #[cfg(not(target_arch = "wasm32"))] use gpui::{AnyElement, Entity, IntoElement}; -use gpui::{ - App, Context, Div, InteractiveElement, ParentElement, Stateful, Styled, Window, div, px, -}; +use gpui_kit::base::StyledExt as _; #[cfg(not(target_arch = "wasm32"))] use gpui_kit::component::Disableable; use gpui_kit::component::button::{Button, ButtonVariant, ButtonVariants}; +use gpui_kit::component::clipboard::Clipboard; use gpui_kit::component::dialog::DialogButtonProps; use gpui_kit::component::menu::{DropdownMenu, PopupMenuItem}; +use gpui_kit::component::popover::Popover; +use gpui_kit::component::separator::Separator; use gpui_kit::component::{ActiveTheme, Icon, Selectable, Sizable, WindowExt, h_flex, v_flex}; use nostr_sdk::prelude::*; +use person::shorten_pubkey; +use settings::AppSettings; use state::{CLIENT_NAME, NostrRegistry}; use ui::{Avatar, IconName}; @@ -31,6 +38,10 @@ impl Sidebar { let updater = AutoUpdater::try_global(cx); let profile = self.directory.read(cx).person(current_user, cx); let avatar = profile.avatar(); + let encryption_key = profile + .announcement() + .map(|announcement| announcement.public_key()); + let encryption_enabled = AppSettings::get_nip4e(cx); h_flex() .id("sidebar-user") @@ -71,6 +82,11 @@ impl Sidebar { }); actions + .when(encryption_enabled, |actions| { + actions.when_some(encryption_key, |actions, encryption_key| { + actions.child(self.render_encryption(encryption_key)) + }) + }) .child( Button::new("sidebar-search") .icon(IconName::Search) @@ -111,6 +127,89 @@ impl Sidebar { }) } + /// The encryption key popover: public key with copy, backup and reset + fn render_encryption(&self, encryption_key: PublicKey) -> impl IntoElement { + Popover::new("sidebar-encryption") + .anchor(Anchor::TopRight) + .trigger( + Button::new("encryption-key") + .icon(IconName::Lock) + .tooltip("Encryption Key") + .ghost() + .small(), + ) + .content(move |_, _, cx| { + let popover = cx.entity(); + let Ok(npub) = encryption_key.to_bech32(); + + v_flex() + .w_64() + .gap_2() + .text_sm() + .child( + div() + .text_xs() + .font_semibold() + .text_color(cx.theme().muted_foreground) + .child("Public key"), + ) + .child( + h_flex() + .items_center() + .gap_1() + .child( + div() + .flex_1() + .min_w_0() + .truncate() + .text_color(cx.theme().muted_foreground) + .child(SharedString::from(shorten_pubkey(encryption_key, 12))), + ) + .child( + Clipboard::new("encryption-key-copy") + .tooltip("Copy public key") + .value(npub), + ), + ) + .child(Separator::horizontal()) + .child( + h_flex() + .justify_end() + .gap_2() + .child( + Button::new("encryption-backup") + .label("Backup") + .outline() + .small() + .on_click({ + let popover = popover.clone(); + move |_, window, cx| { + popover + .update(cx, |state, cx| state.dismiss(window, cx)); + window.dispatch_action( + Box::new(Command::BackupEncryption), + cx, + ); + } + }), + ) + .child( + Button::new("encryption-reset") + .label("Reset") + .ghost() + .small() + .on_click(move |_, window, cx| { + popover.update(cx, |state, cx| state.dismiss(window, cx)); + window.dispatch_action( + Box::new(Command::ResetEncryption), + cx, + ); + }), + ), + ) + }) + } + pub(super) fn render_footer(&self, loading: bool, _cx: &mut Context) -> Div { v_flex() .p_2() diff --git a/crates/workspace/src/sidebar/mod.rs b/crates/workspace/src/sidebar/mod.rs index 83b21fe5..5fc32c41 100644 --- a/crates/workspace/src/sidebar/mod.rs +++ b/crates/workspace/src/sidebar/mod.rs @@ -21,6 +21,7 @@ use nostr_sdk::prelude::*; use person::{Directory, PersonRegistry}; pub(crate) use rows::open_room; use search::SearchResult; +use settings::AppSettings; use smallvec::{SmallVec, smallvec}; use state::{FIND_DELAY, NostrRegistry, StateEvent}; use ui::title_bar_drag_handlers; @@ -104,6 +105,8 @@ impl Sidebar { let chat = ChatRegistry::global(cx); let groups = GroupsRegistry::global(cx); let nostr = NostrRegistry::global(cx); + let settings = AppSettings::global(cx); + let directory = Directory::new(cx); let find_input = cx.new(|cx| { InputState::new(window, cx) @@ -206,6 +209,11 @@ impl Sidebar { }, )); + // The user header reads settings and the tracked profiles on render, so + // the encryption button reacts to toggles and announcement arrivals + subscriptions.push(cx.observe(&settings, |_, _, cx| cx.notify())); + subscriptions.push(cx.observe(&directory, |_, _, cx| cx.notify())); + tasks.push(cx.spawn(async move |this, cx| { let timer = Duration::from_millis(PROGRESS_TICK_MS); loop { @@ -229,7 +237,7 @@ impl Sidebar { chats_open: None, contacts_open: false, contacts: None, - directory: Directory::new(cx), + directory, selected_contacts: HashSet::new(), requests_open: false, new_requests: false,