use std::time::Duration; use anyhow::Error; use gpui::{ AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window, div, }; use nostr_sdk::prelude::*; use state::KEYRING; use theme::ActiveTheme; use ui::button::{Button, ButtonVariants}; use ui::dock::{Panel, PanelEvent}; use ui::input::{InputState, TextInput}; use ui::{IconName, Sizable, StyledExt, divider, v_flex}; const MSG: &str = "Store your account keys in a safe location. \ You can restore your account or move to another client anytime you want."; pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| BackupPanel::new(window, cx)) } #[derive(Debug)] pub struct BackupPanel { name: SharedString, focus_handle: FocusHandle, /// Public key input npub_input: Entity, /// Secret key input nsec_input: Entity, /// Copied status copied: bool, /// Background tasks tasks: Vec>>, } impl BackupPanel { pub fn new(window: &mut Window, cx: &mut Context) -> Self { let npub_input = cx.new(|cx| InputState::new(window, cx).disabled(true)); let nsec_input = cx.new(|cx| InputState::new(window, cx).disabled(true).masked(true)); // Run at the end of current cycle cx.defer_in(window, |this, window, cx| { this.load(window, cx); }); Self { name: "Backup".into(), focus_handle: cx.focus_handle(), npub_input, nsec_input, copied: false, tasks: vec![], } } fn load(&mut self, window: &mut Window, cx: &mut Context) { let keyring = cx.read_credentials(KEYRING); self.tasks.push(cx.spawn_in(window, async move |this, cx| { if let Some((_, secret)) = keyring.await? { let secret = SecretKey::from_slice(&secret)?; let keys = Keys::new(secret); this.update_in(cx, |this, window, cx| { this.npub_input.update(cx, |this, cx| { this.set_value(keys.public_key().to_bech32().unwrap(), window, cx); }); this.nsec_input.update(cx, |this, cx| { this.set_value(keys.secret_key().to_bech32().unwrap(), window, cx); }); })?; } Ok(()) })); } fn copy_secret(&mut self, cx: &mut Context) { let value = self.nsec_input.read(cx).value(); let item = ClipboardItem::new_string(value.to_string()); cx.write_to_clipboard(item); // Set the copied status to true self.set_copied(true, cx); } fn set_copied(&mut self, status: bool, cx: &mut Context) { self.copied = status; cx.notify(); self.tasks.push(cx.spawn(async move |this, cx| { cx.background_executor().timer(Duration::from_secs(2)).await; // Clear the error message after a delay this.update(cx, |this, cx| { this.set_copied(false, cx); })?; Ok(()) })); } } impl Panel for BackupPanel { fn panel_id(&self) -> SharedString { self.name.clone() } fn title(&self, _cx: &App) -> AnyElement { self.name.clone().into_any_element() } } impl EventEmitter for BackupPanel {} impl Focusable for BackupPanel { fn focus_handle(&self, _: &App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl Render for BackupPanel { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { 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_2() .flex_1() .w_full() .text_sm() .child( v_flex() .gap_1p5() .w_full() .child( div() .text_xs() .font_semibold() .text_color(cx.theme().text_muted) .child(SharedString::from("Public Key:")), ) .child( TextInput::new(&self.npub_input) .small() .bordered(false) .disabled(true), ), ) .child( v_flex() .gap_1p5() .w_full() .child( div() .text_xs() .font_semibold() .text_color(cx.theme().text_muted) .child(SharedString::from("Secret Key:")), ) .child( TextInput::new(&self.nsec_input) .small() .bordered(false) .disabled(true), ), ) .child( Button::new("copy") .icon(IconName::Copy) .label({ if self.copied { "Copied" } else { "Copy secret key" } }) .primary() .small() .font_semibold() .on_click(cx.listener(move |this, _ev, _window, cx| { this.copy_secret(cx); })), ), ) } }