feat: add identity import dialog
This commit is contained in:
@@ -250,6 +250,119 @@ impl Backend {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Import an `nsec1...` secret key, storing it NIP-49 encrypted with `password`.
|
||||||
|
pub fn import_nsec(
|
||||||
|
&mut self,
|
||||||
|
nsec: &str,
|
||||||
|
password: &str,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Task<Result<PublicKey, Error>> {
|
||||||
|
let keys = match SecretKey::parse(nsec.trim()) {
|
||||||
|
Ok(secret) => Keys::new(secret),
|
||||||
|
Err(e) => return Task::ready(Err(anyhow!(e))),
|
||||||
|
};
|
||||||
|
|
||||||
|
if password.is_empty() {
|
||||||
|
return Task::ready(Err(anyhow!("Passphrase must not be empty")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = password.to_owned();
|
||||||
|
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let job = cx.background_spawn(async move {
|
||||||
|
let encrypted =
|
||||||
|
EncryptedSecretKey::new(keys.secret_key(), &password, 16, KeySecurity::Medium)?;
|
||||||
|
let ncryptsec = encrypted.to_bech32()?;
|
||||||
|
Ok::<_, Error>((keys, ncryptsec))
|
||||||
|
});
|
||||||
|
|
||||||
|
let (keys, ncryptsec) = job.await?;
|
||||||
|
let public_key = keys.public_key();
|
||||||
|
|
||||||
|
let write = cx.update(|cx| {
|
||||||
|
cx.write_credentials(USER_KEYRING, &public_key.to_hex(), ncryptsec.as_bytes())
|
||||||
|
});
|
||||||
|
write.await?;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| this.set_signer(keys, cx))?;
|
||||||
|
|
||||||
|
Ok(public_key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import an NIP-49 encrypted secret key (`ncryptsec1...`), decrypting it with `password`.
|
||||||
|
pub fn import_ncryptsec(
|
||||||
|
&mut self,
|
||||||
|
ncryptsec: &str,
|
||||||
|
password: &str,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Task<Result<PublicKey, Error>> {
|
||||||
|
let ncryptsec = ncryptsec.trim().to_owned();
|
||||||
|
|
||||||
|
if password.is_empty() {
|
||||||
|
return Task::ready(Err(anyhow!("Passphrase must not be empty")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = password.to_owned();
|
||||||
|
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let stored = ncryptsec.clone();
|
||||||
|
let decrypt_task = cx.background_spawn(async move {
|
||||||
|
let encrypted = EncryptedSecretKey::from_bech32(&ncryptsec)?;
|
||||||
|
let secret = encrypted.decrypt(&password)?;
|
||||||
|
Ok::<_, Error>(Keys::new(secret))
|
||||||
|
});
|
||||||
|
|
||||||
|
let keys = decrypt_task.await?;
|
||||||
|
let public_key = keys.public_key();
|
||||||
|
|
||||||
|
let write = cx.update(|cx| {
|
||||||
|
cx.write_credentials(USER_KEYRING, &public_key.to_hex(), stored.as_bytes())
|
||||||
|
});
|
||||||
|
write.await?;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| this.set_signer(keys, cx))?;
|
||||||
|
|
||||||
|
Ok(public_key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import a `bunker://...` URI (NIP-46), connecting to the remote signer.
|
||||||
|
pub fn import_bunker(
|
||||||
|
&mut self,
|
||||||
|
uri: &str,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Task<Result<PublicKey, Error>> {
|
||||||
|
let uri_string = uri.trim().to_owned();
|
||||||
|
|
||||||
|
let connect_uri = match NostrConnectUri::parse(&uri_string) {
|
||||||
|
Ok(uri) => uri,
|
||||||
|
Err(e) => return Task::ready(Err(anyhow!(e))),
|
||||||
|
};
|
||||||
|
|
||||||
|
let keys = Keys::generate();
|
||||||
|
let credential = with_master_key(&uri_string, &keys);
|
||||||
|
let write = cx.write_credentials(USER_KEYRING, "bunker", credential.as_bytes());
|
||||||
|
|
||||||
|
cx.spawn(async move |this, cx| {
|
||||||
|
let mut signer = NostrConnect::new(
|
||||||
|
connect_uri,
|
||||||
|
keys,
|
||||||
|
Duration::from_secs(NOSTR_CONNECT_TIMEOUT),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
signer.auth_url_handler(SignedAuthUrlHandler);
|
||||||
|
|
||||||
|
// Verify the bunker responds before persisting the credential.
|
||||||
|
let public_key = signer.get_public_key_async().await?;
|
||||||
|
write.await?;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| this.set_signer(signer, cx))?;
|
||||||
|
|
||||||
|
Ok(public_key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn create_identity(
|
pub fn create_identity(
|
||||||
&mut self,
|
&mut self,
|
||||||
name: &str,
|
name: &str,
|
||||||
@@ -568,8 +681,7 @@ impl Backend {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Errors when no grasp server accepted the push; the outcome reports
|
// Errors when no grasp server accepted the push.
|
||||||
// which servers did when only some accepted it.
|
|
||||||
pub fn push_repository(
|
pub fn push_repository(
|
||||||
&mut self,
|
&mut self,
|
||||||
announcement: Announcement,
|
announcement: Announcement,
|
||||||
@@ -614,8 +726,6 @@ impl Backend {
|
|||||||
let relays = announcement.relays.clone();
|
let relays = announcement.relays.clone();
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
// Runs on completion, on error and on cancellation alike; the
|
|
||||||
// guard would be dropped with the task if not held.
|
|
||||||
let _guard = cx.on_drop(&this, {
|
let _guard = cx.on_drop(&this, {
|
||||||
let addr = addr.clone();
|
let addr = addr.clone();
|
||||||
move |backend, cx| {
|
move |backend, cx| {
|
||||||
@@ -634,8 +744,7 @@ impl Backend {
|
|||||||
work.await?
|
work.await?
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keep the announced default branch in `HEAD` when it is among
|
// Keep the announced default branch in `HEAD` when it is among the pushed refs.
|
||||||
// the pushed refs; otherwise `HEAD` stays the checkout's branch.
|
|
||||||
let heads: Vec<&str> = state
|
let heads: Vec<&str> = state
|
||||||
.refs
|
.refs
|
||||||
.iter()
|
.iter()
|
||||||
@@ -648,8 +757,7 @@ impl Backend {
|
|||||||
state.head = Some(head);
|
state.head = Some(head);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grasp servers authorize a push by the state event they hold in
|
// Grasp servers authorize a push by the state event they hold in purgatory.
|
||||||
// purgatory: stage it on each server's relay before the git push.
|
|
||||||
let refs = state.refs.clone();
|
let refs = state.refs.clone();
|
||||||
let head = state.head.clone();
|
let head = state.head.clone();
|
||||||
|
|
||||||
@@ -960,8 +1068,7 @@ impl Backend {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each target gets its own deletion event: a relay rejecting or dropping
|
// Each target gets its own deletion event.
|
||||||
// one does not affect the others.
|
|
||||||
fn retract_events(&mut self, events: &[Event], cx: &mut Context<Self>) {
|
fn retract_events(&mut self, events: &[Event], cx: &mut Context<Self>) {
|
||||||
let pusher = GraspPush::new(self.client.clone(), self.signer.clone());
|
let pusher = GraspPush::new(self.client.clone(), self.signer.clone());
|
||||||
|
|
||||||
@@ -1001,10 +1108,7 @@ fn repository_announcement(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Announce the repository, then stage the state event and push: the state
|
// Announce the repository, then stage the state event and push.
|
||||||
// event is the push authorization, so it must be accepted before the push.
|
|
||||||
// Connect to all servers first — the nostr client queues events until each
|
|
||||||
// relay is connected.
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn announce_repository_and_push(
|
async fn announce_repository_and_push(
|
||||||
backend: &WeakEntity<Backend>,
|
backend: &WeakEntity<Backend>,
|
||||||
@@ -1031,10 +1135,7 @@ async fn announce_repository_and_push(
|
|||||||
GraspPush::require_relay_accepted(output, event)?
|
GraspPush::require_relay_accepted(output, event)?
|
||||||
};
|
};
|
||||||
|
|
||||||
// The state event is the push authorization. Stage it on each grasp
|
// The state event is the push authorization.
|
||||||
// server's relay, then push the git data. The push fails only when no
|
|
||||||
// server accepted it. The announcement is then retracted so the
|
|
||||||
// repository is not left announced without content.
|
|
||||||
let outcome = if refs.is_empty() {
|
let outcome = if refs.is_empty() {
|
||||||
PushOutcome::default()
|
PushOutcome::default()
|
||||||
} else {
|
} else {
|
||||||
@@ -1063,8 +1164,7 @@ async fn announce_repository_and_push(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if outcome.accepted() == 0 {
|
if outcome.accepted() == 0 {
|
||||||
// Retract the announcement so the repository is not left announced
|
// Retract the announcement so the repository is not left announced without content.
|
||||||
// without content.
|
|
||||||
backend
|
backend
|
||||||
.update(cx, |backend, cx| {
|
.update(cx, |backend, cx| {
|
||||||
backend.retract_events(std::slice::from_ref(&event), cx);
|
backend.retract_events(std::slice::from_ref(&event), cx);
|
||||||
@@ -1135,3 +1235,9 @@ fn extract_master_key(credential: &str) -> (&str, Keys) {
|
|||||||
None => (credential, Keys::generate()),
|
None => (credential, Keys::generate()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn with_master_key(uri: &str, keys: &Keys) -> String {
|
||||||
|
let separator = if uri.contains('?') { '&' } else { '?' };
|
||||||
|
let nsec = keys.secret_key().to_bech32().expect("infallible");
|
||||||
|
format!("{uri}{separator}master={nsec}")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,337 @@
|
|||||||
use gpui::{App, Window, px};
|
use gpui::prelude::*;
|
||||||
use gpui_component::WindowExt;
|
use gpui::{AnyWindowHandle, App, Entity, Subscription, Task, Window, px};
|
||||||
|
use gpui_component::button::{Button, ButtonVariants};
|
||||||
|
use gpui_component::dialog::{DialogDescription, DialogFooter, DialogHeader, DialogTitle};
|
||||||
|
use gpui_component::form::{field, v_form};
|
||||||
|
use gpui_component::input::{Input, InputEvent, InputState};
|
||||||
|
use gpui_component::{Disableable, WindowExt};
|
||||||
|
use signed_state::Backend;
|
||||||
|
|
||||||
|
use crate::views::dialog_state::{DialogProgress, error_row};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default)]
|
||||||
|
enum Stage {
|
||||||
|
#[default]
|
||||||
|
Credential,
|
||||||
|
/// An `nsec` was entered; ask for a passphrase to encrypt it with.
|
||||||
|
Encrypt,
|
||||||
|
/// An `ncryptsec` was entered; ask for its passphrase to decrypt it.
|
||||||
|
Decrypt,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The credential entered on the first step.
|
||||||
|
enum Credential {
|
||||||
|
Nsec(String),
|
||||||
|
Ncryptsec(String),
|
||||||
|
Bunker(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Credential {
|
||||||
|
fn parse(input: &str) -> Option<Self> {
|
||||||
|
let input = input.trim();
|
||||||
|
|
||||||
|
if input.starts_with("nsec1") {
|
||||||
|
Some(Self::Nsec(input.to_owned()))
|
||||||
|
} else if input.starts_with("ncryptsec1") {
|
||||||
|
Some(Self::Ncryptsec(input.to_owned()))
|
||||||
|
} else if input.starts_with("bunker://") {
|
||||||
|
Some(Self::Bunker(input.to_owned()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State of the import dialog, so the current step and async results can be rendered.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ImportState {
|
||||||
|
stage: Stage,
|
||||||
|
credential: String,
|
||||||
|
progress: DialogProgress,
|
||||||
|
/// Keeps the subscriptions alive while the dialog is open.
|
||||||
|
subscriptions: Vec<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn open(window: &mut Window, cx: &mut App) {
|
pub fn open(window: &mut Window, cx: &mut App) {
|
||||||
|
let secret_input =
|
||||||
|
cx.new(|cx| InputState::new(window, cx).placeholder("secret key or bunker://"));
|
||||||
|
|
||||||
|
let pass_input = cx.new(|cx| {
|
||||||
|
InputState::new(window, cx)
|
||||||
|
.placeholder("Passphrase to protect your key")
|
||||||
|
.masked(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
let repass_input = cx.new(|cx| {
|
||||||
|
InputState::new(window, cx)
|
||||||
|
.placeholder("Repeat passphrase")
|
||||||
|
.masked(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
let handle = window.window_handle();
|
||||||
|
let state = cx.new(|_| ImportState::default());
|
||||||
|
|
||||||
|
// Enter in any field submits the step the dialog is on.
|
||||||
|
let mut subscriptions = Vec::new();
|
||||||
|
|
||||||
|
for input in [&secret_input, &pass_input, &repass_input] {
|
||||||
|
let secret_input = secret_input.clone();
|
||||||
|
let pass_input = pass_input.clone();
|
||||||
|
let repass_input = repass_input.clone();
|
||||||
|
let state = state.clone();
|
||||||
|
|
||||||
|
subscriptions.push(cx.subscribe(input, move |_input, event, cx| {
|
||||||
|
if matches!(event, InputEvent::PressEnter { .. }) {
|
||||||
|
submit(
|
||||||
|
&secret_input,
|
||||||
|
&pass_input,
|
||||||
|
&repass_input,
|
||||||
|
&state,
|
||||||
|
&handle,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
state.update(cx, |state, _cx| {
|
||||||
|
state.subscriptions = subscriptions;
|
||||||
|
});
|
||||||
|
|
||||||
window.open_dialog(cx, move |dialog, _window, _cx| {
|
window.open_dialog(cx, move |dialog, _window, _cx| {
|
||||||
dialog.title("Import identity").width(px(400.))
|
let secret_input = secret_input.clone();
|
||||||
|
let pass_input = pass_input.clone();
|
||||||
|
let repass_input = repass_input.clone();
|
||||||
|
let state = state.clone();
|
||||||
|
|
||||||
|
dialog
|
||||||
|
.width(px(520.))
|
||||||
|
.margin_top(px(50.))
|
||||||
|
.content(move |content, _window, cx| {
|
||||||
|
let stage = state.read(cx).stage;
|
||||||
|
let busy = state.read(cx).progress.busy;
|
||||||
|
let error = state.read(cx).progress.error.clone();
|
||||||
|
|
||||||
|
content
|
||||||
|
.child(
|
||||||
|
DialogHeader::new()
|
||||||
|
.child(DialogTitle::new().child("Import identity"))
|
||||||
|
.child(DialogDescription::new().child(match stage {
|
||||||
|
Stage::Credential => {
|
||||||
|
"Paste the secret key or bunker URI of an existing identity."
|
||||||
|
}
|
||||||
|
Stage::Encrypt => {
|
||||||
|
"Choose a passphrase to encrypt your key on this device."
|
||||||
|
}
|
||||||
|
Stage::Decrypt => "Enter the passphrase used to encrypt this key.",
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(match stage {
|
||||||
|
Stage::Credential => v_form().child(
|
||||||
|
field()
|
||||||
|
.label("Secret key or bunker URI")
|
||||||
|
.required(true)
|
||||||
|
.child(Input::new(&secret_input)),
|
||||||
|
),
|
||||||
|
Stage::Encrypt => v_form()
|
||||||
|
.child(
|
||||||
|
field()
|
||||||
|
.label("Passphrase")
|
||||||
|
.required(true)
|
||||||
|
.child(Input::new(&pass_input)),
|
||||||
|
)
|
||||||
|
.child(field().required(true).child(Input::new(&repass_input))),
|
||||||
|
Stage::Decrypt => v_form().child(
|
||||||
|
field()
|
||||||
|
.label("Passphrase")
|
||||||
|
.required(true)
|
||||||
|
.child(Input::new(&pass_input)),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.children(error_row(&error, cx))
|
||||||
|
.child(
|
||||||
|
DialogFooter::new().justify_end().child(
|
||||||
|
Button::new("import")
|
||||||
|
.primary()
|
||||||
|
.label(match stage {
|
||||||
|
Stage::Credential => "Continue",
|
||||||
|
Stage::Encrypt => "Import identity",
|
||||||
|
Stage::Decrypt => "Unlock",
|
||||||
|
})
|
||||||
|
.loading(busy)
|
||||||
|
.disabled(busy)
|
||||||
|
.on_click({
|
||||||
|
let secret_input = secret_input.clone();
|
||||||
|
let pass_input = pass_input.clone();
|
||||||
|
let repass_input = repass_input.clone();
|
||||||
|
let state = state.clone();
|
||||||
|
|
||||||
|
move |_ev, _window, cx| {
|
||||||
|
submit(
|
||||||
|
&secret_input,
|
||||||
|
&pass_input,
|
||||||
|
&repass_input,
|
||||||
|
&state,
|
||||||
|
&handle,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn submit(
|
||||||
|
secret_input: &Entity<InputState>,
|
||||||
|
pass_input: &Entity<InputState>,
|
||||||
|
repass_input: &Entity<InputState>,
|
||||||
|
state: &Entity<ImportState>,
|
||||||
|
handle: &AnyWindowHandle,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
if state.read(cx).progress.busy {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
match state.read(cx).stage {
|
||||||
|
Stage::Credential => start_import(secret_input, state, handle, cx),
|
||||||
|
Stage::Encrypt => import_nsec(pass_input, repass_input, state, handle, cx),
|
||||||
|
Stage::Decrypt => import_ncryptsec(pass_input, state, handle, cx),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the entered credential and either moves to the passphrase step or connects to a bunker.
|
||||||
|
fn start_import(
|
||||||
|
secret_input: &Entity<InputState>,
|
||||||
|
state: &Entity<ImportState>,
|
||||||
|
handle: &AnyWindowHandle,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
let credential = match Credential::parse(&secret_input.read(cx).value()) {
|
||||||
|
Some(credential) => credential,
|
||||||
|
None => {
|
||||||
|
state.update(cx, |state, _| {
|
||||||
|
state
|
||||||
|
.progress
|
||||||
|
.fail("Enter an nsec, ncryptsec or bunker:// URI");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match credential {
|
||||||
|
Credential::Nsec(secret) => {
|
||||||
|
state.update(cx, |state, _| {
|
||||||
|
state.credential = secret;
|
||||||
|
state.stage = Stage::Encrypt;
|
||||||
|
state.progress.error = None;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Credential::Ncryptsec(secret) => {
|
||||||
|
state.update(cx, |state, _| {
|
||||||
|
state.credential = secret;
|
||||||
|
state.stage = Stage::Decrypt;
|
||||||
|
state.progress.error = None;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Credential::Bunker(uri) => {
|
||||||
|
state.update(cx, |state, _| state.progress.begin());
|
||||||
|
|
||||||
|
let backend = Backend::global(cx);
|
||||||
|
let task = backend.update(cx, |backend, cx| backend.import_bunker(&uri, cx));
|
||||||
|
finish_import(task, state, handle, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encrypts the entered `nsec` with the passphrase and signs in with it.
|
||||||
|
fn import_nsec(
|
||||||
|
pass_input: &Entity<InputState>,
|
||||||
|
repass_input: &Entity<InputState>,
|
||||||
|
state: &Entity<ImportState>,
|
||||||
|
handle: &AnyWindowHandle,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
let backend = Backend::global(cx);
|
||||||
|
let nsec = state.read(cx).credential.clone();
|
||||||
|
let pass = pass_input.read(cx).value().to_string();
|
||||||
|
let repass = repass_input.read(cx).value().to_string();
|
||||||
|
|
||||||
|
if pass.is_empty() {
|
||||||
|
state.update(cx, |state, _| {
|
||||||
|
state.progress.fail("Passphrase must not be empty");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if pass != repass {
|
||||||
|
state.update(cx, |state, _| {
|
||||||
|
state.progress.fail("Passphrases do not match");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.update(cx, |state, _cx| {
|
||||||
|
state.progress.begin();
|
||||||
|
});
|
||||||
|
|
||||||
|
let task = backend.update(cx, |backend, cx| backend.import_nsec(&nsec, &pass, cx));
|
||||||
|
finish_import(task, state, handle, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypts the entered `ncryptsec` with the passphrase and signs in with it.
|
||||||
|
fn import_ncryptsec(
|
||||||
|
pass_input: &Entity<InputState>,
|
||||||
|
state: &Entity<ImportState>,
|
||||||
|
handle: &AnyWindowHandle,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
let backend = Backend::global(cx);
|
||||||
|
let ncryptsec = state.read(cx).credential.clone();
|
||||||
|
let pass = pass_input.read(cx).value().to_string();
|
||||||
|
|
||||||
|
if pass.is_empty() {
|
||||||
|
state.update(cx, |state, _| {
|
||||||
|
state.progress.fail("Passphrase must not be empty");
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.update(cx, |state, _cx| {
|
||||||
|
state.progress.begin();
|
||||||
|
});
|
||||||
|
|
||||||
|
let task = backend.update(cx, |backend, cx| {
|
||||||
|
backend.import_ncryptsec(&ncryptsec, &pass, cx)
|
||||||
|
});
|
||||||
|
finish_import(task, state, handle, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the import task to completion, closing the dialog on success.
|
||||||
|
fn finish_import<T: 'static>(
|
||||||
|
task: Task<Result<T, anyhow::Error>>,
|
||||||
|
state: &Entity<ImportState>,
|
||||||
|
handle: &AnyWindowHandle,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
let handle = *handle;
|
||||||
|
let state = state.clone();
|
||||||
|
|
||||||
|
cx.spawn(async move |cx| match task.await {
|
||||||
|
Ok(_) => {
|
||||||
|
cx.update_window(handle, |_, window, cx| {
|
||||||
|
window.close_dialog(cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
cx.update_window(handle, |_, _window, cx| {
|
||||||
|
state.update(cx, |state, _| state.progress.fail(e.to_string()));
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|||||||
@@ -610,7 +610,7 @@ impl SidebarPanel {
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
BaseButton::new("onboarding")
|
BaseButton::new("import")
|
||||||
.h_flex()
|
.h_flex()
|
||||||
.h_8()
|
.h_8()
|
||||||
.px_2()
|
.px_2()
|
||||||
|
|||||||
Reference in New Issue
Block a user