add backup panel
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m47s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m52s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled

This commit is contained in:
2026-02-25 09:11:23 +07:00
parent 6d863d8bbe
commit 971a82df1b
10 changed files with 308 additions and 123 deletions

View File

@@ -0,0 +1,194 @@
use std::time::Duration;
use anyhow::Error;
use gpui::{
div, AnyElement, App, AppContext, ClipboardItem, Context, Entity, EventEmitter, FocusHandle,
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Task, Window,
};
use nostr_sdk::prelude::*;
use state::KEYRING;
use theme::ActiveTheme;
use ui::button::{Button, ButtonVariants};
use ui::dock_area::panel::{Panel, PanelEvent};
use ui::input::{InputState, TextInput};
use ui::{divider, v_flex, IconName, Sizable, StyledExt};
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<BackupPanel> {
cx.new(|cx| BackupPanel::new(window, cx))
}
#[derive(Debug)]
pub struct BackupPanel {
name: SharedString,
focus_handle: FocusHandle,
/// Public key input
npub_input: Entity<InputState>,
/// Secret key input
nsec_input: Entity<InputState>,
/// Copied status
copied: bool,
/// Background tasks
tasks: Vec<Task<Result<(), Error>>>,
}
impl BackupPanel {
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> 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));
// 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<Self>) {
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_key(&mut self, cx: &mut Context<Self>) {
let value = self.nsec_input.read(cx).value();
let item = ClipboardItem::new_string(value.to_string());
// Copy to clipboard
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>) {
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<PanelEvent> 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<Self>) -> 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)),
)
.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)),
)
.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_key(cx);
})),
),
)
}
}

View File

@@ -144,16 +144,17 @@ impl MessagingRelayPanel {
self.error = Some(error.into());
cx.notify();
cx.spawn_in(window, async move |this, cx| {
self.tasks.push(cx.spawn_in(window, 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.error = None;
cx.notify();
})
.ok();
})
.detach();
})?;
Ok(())
}));
}
fn set_updating(&mut self, updating: bool, cx: &mut Context<Self>) {

View File

@@ -1,3 +1,4 @@
pub mod backup;
pub mod connect;
pub mod encryption_key;
pub mod greeter;

View File

@@ -163,16 +163,17 @@ impl RelayListPanel {
self.error = Some(error.into());
cx.notify();
cx.spawn_in(window, async move |this, cx| {
self.tasks.push(cx.spawn_in(window, 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.error = None;
cx.notify();
})
.ok();
})
.detach();
})?;
Ok(())
}));
}
fn set_updating(&mut self, updating: bool, cx: &mut Context<Self>) {

View File

@@ -20,7 +20,7 @@ use ui::dock_area::{ClosePanel, DockArea, DockItem};
use ui::menu::DropdownMenu;
use ui::{h_flex, v_flex, IconName, Root, Sizable, WindowExtension};
use crate::panels::{encryption_key, greeter, messaging_relays, relay_list};
use crate::panels::{backup, encryption_key, greeter, messaging_relays, profile, relay_list};
use crate::sidebar;
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
@@ -30,11 +30,17 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Workspace> {
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
#[action(namespace = workspace, no_json)]
enum Command {
ReloadRelayList,
OpenRelayPanel,
ReloadInbox,
OpenInboxPanel,
OpenEncryptionPanel,
ToggleTheme,
RefreshRelayList,
RefreshMessagingRelays,
ShowRelayList,
ShowMessaging,
ShowEncryption,
ShowProfile,
ShowSettings,
ShowBackup,
}
pub struct Workspace {
@@ -181,7 +187,32 @@ impl Workspace {
fn on_command(&mut self, command: &Command, window: &mut Window, cx: &mut Context<Self>) {
match command {
Command::OpenEncryptionPanel => {
Command::ShowProfile => {
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(profile::init(public_key, window, cx)),
DockPlacement::Right,
window,
cx,
);
});
}
}
Command::ShowBackup => {
self.dock.update(cx, |this, cx| {
this.add_panel(
Arc::new(backup::init(window, cx)),
DockPlacement::Right,
window,
cx,
);
});
}
Command::ShowEncryption => {
let nostr = NostrRegistry::global(cx);
let signer = nostr.read(cx).signer();
@@ -196,7 +227,7 @@ impl Workspace {
});
}
}
Command::OpenInboxPanel => {
Command::ShowMessaging => {
self.dock.update(cx, |this, cx| {
this.add_panel(
Arc::new(messaging_relays::init(window, cx)),
@@ -206,7 +237,7 @@ impl Workspace {
);
});
}
Command::OpenRelayPanel => {
Command::ShowRelayList => {
self.dock.update(cx, |this, cx| {
this.add_panel(
Arc::new(relay_list::init(window, cx)),
@@ -216,18 +247,19 @@ impl Workspace {
);
});
}
Command::ReloadInbox => {
Command::RefreshRelayList => {
let nostr = NostrRegistry::global(cx);
nostr.update(cx, |this, cx| {
this.ensure_relay_list(cx);
});
}
Command::ReloadRelayList => {
Command::RefreshMessagingRelays => {
let chat = ChatRegistry::global(cx);
chat.update(cx, |this, cx| {
this.ensure_messaging_relays(cx);
});
}
_ => {}
}
}
@@ -252,12 +284,29 @@ impl Workspace {
.compact()
.transparent()
.dropdown_menu(move |this, _window, _cx| {
this.label(profile.name())
this.min_w(px(256.))
.label(profile.name())
.separator()
.menu("Profile", Box::new(ClosePanel))
.menu("Backup", Box::new(ClosePanel))
.menu("Themes", Box::new(ClosePanel))
.menu("Settings", Box::new(ClosePanel))
.menu_with_icon(
"Profile",
IconName::Profile,
Box::new(Command::ShowProfile),
)
.menu_with_icon(
"Backup",
IconName::UserKey,
Box::new(Command::ShowBackup),
)
.menu_with_icon(
"Themes",
IconName::Sun,
Box::new(Command::ToggleTheme),
)
.menu_with_icon(
"Settings",
IconName::Settings,
Box::new(Command::ShowSettings),
)
}),
)
})
@@ -298,7 +347,7 @@ impl Workspace {
.small()
.ghost()
.on_click(|_ev, window, cx| {
window.dispatch_action(Box::new(Command::OpenEncryptionPanel), cx);
window.dispatch_action(Box::new(Command::ShowEncryption), cx);
}),
)
.child(
@@ -333,7 +382,7 @@ impl Workspace {
this.min_w(px(260.))
.label("Messaging Relays")
.menu_element_with_disabled(
Box::new(Command::OpenRelayPanel),
Box::new(Command::ShowRelayList),
true,
move |_window, cx| {
let persons = PersonRegistry::global(cx);
@@ -380,12 +429,12 @@ impl Workspace {
.menu_with_icon(
"Reload",
IconName::Refresh,
Box::new(Command::ReloadInbox),
Box::new(Command::RefreshMessagingRelays),
)
.menu_with_icon(
"Update relays",
IconName::Settings,
Box::new(Command::OpenInboxPanel),
Box::new(Command::ShowMessaging),
)
}),
),
@@ -421,7 +470,7 @@ impl Workspace {
this.min_w(px(260.))
.label("Relays")
.menu_element_with_disabled(
Box::new(Command::OpenRelayPanel),
Box::new(Command::ShowRelayList),
true,
move |_window, cx| {
let nostr = NostrRegistry::global(cx);
@@ -465,12 +514,12 @@ impl Workspace {
.menu_with_icon(
"Reload",
IconName::Refresh,
Box::new(Command::ReloadRelayList),
Box::new(Command::RefreshRelayList),
)
.menu_with_icon(
"Update relay list",
IconName::Settings,
Box::new(Command::OpenRelayPanel),
Box::new(Command::ShowRelayList),
)
}),
),