feat: implement multiple keystores (#187)
* keystore * . * fix * . * allow user disable keyring * update texts
This commit is contained in:
@@ -35,7 +35,6 @@ common = { path = "../common" }
|
||||
states = { path = "../states" }
|
||||
registry = { path = "../registry" }
|
||||
settings = { path = "../settings" }
|
||||
client_keys = { path = "../client_keys" }
|
||||
auto_update = { path = "../auto_update" }
|
||||
|
||||
rust-i18n.workspace = true
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use gpui::{actions, App};
|
||||
use gpui::{actions, App, AppContext};
|
||||
use nostr_connect::prelude::*;
|
||||
use registry::keystore::KeyItem;
|
||||
use registry::Registry;
|
||||
use states::app_state;
|
||||
|
||||
actions!(coop, [ReloadMetadata, DarkMode, Settings, Logout, Quit]);
|
||||
actions!(sidebar, [Reload, RelayStatus]);
|
||||
@@ -43,6 +46,36 @@ pub fn load_embedded_fonts(cx: &App) {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn reset(cx: &mut App) {
|
||||
let registry = Registry::global(cx);
|
||||
let keystore = registry.read(cx).keystore();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
cx.background_spawn(async move {
|
||||
let client = app_state().client();
|
||||
client.unset_signer().await;
|
||||
})
|
||||
.await;
|
||||
|
||||
keystore
|
||||
.delete_credentials(&KeyItem::User.to_string(), cx)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
keystore
|
||||
.delete_credentials(&KeyItem::Bunker.to_string(), cx)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
registry
|
||||
.update(cx, |this, cx| {
|
||||
this.reset(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn quit(_: &Quit, cx: &mut App) {
|
||||
log::info!("Gracefully quitting the application . . .");
|
||||
cx.quit();
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Error};
|
||||
use auto_update::AutoUpdater;
|
||||
use client_keys::ClientKeys;
|
||||
use common::display::RenderedProfile;
|
||||
use common::event::EventUtils;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
@@ -17,10 +16,11 @@ use i18n::{shared_t, t};
|
||||
use itertools::Itertools;
|
||||
use nostr_connect::prelude::*;
|
||||
use nostr_sdk::prelude::*;
|
||||
use registry::keystore::KeyItem;
|
||||
use registry::{Registry, RegistryEvent};
|
||||
use settings::AppSettings;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::constants::{ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH};
|
||||
use states::constants::{BOOTSTRAP_RELAYS, DEFAULT_SIDEBAR_WIDTH};
|
||||
use states::state::{AuthRequest, SignalKind, UnwrappingStatus};
|
||||
use states::{app_state, default_nip17_relays, default_nip65_relays};
|
||||
use theme::{ActiveTheme, Theme, ThemeMode};
|
||||
@@ -36,7 +36,7 @@ use ui::notification::Notification;
|
||||
use ui::popup_menu::PopupMenuExt;
|
||||
use ui::{h_flex, v_flex, ContextModal, Disableable, IconName, Root, Sizable, StyledExt};
|
||||
|
||||
use crate::actions::{DarkMode, Logout, ReloadMetadata, Settings};
|
||||
use crate::actions::{reset, DarkMode, Logout, ReloadMetadata, Settings};
|
||||
use crate::views::compose::compose_button;
|
||||
use crate::views::setup_relay::SetupRelay;
|
||||
use crate::views::{
|
||||
@@ -82,7 +82,6 @@ pub struct ChatSpace {
|
||||
|
||||
impl ChatSpace {
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let client_keys = ClientKeys::global(cx);
|
||||
let registry = Registry::global(cx);
|
||||
let status = registry.read(cx).unwrapping_status.clone();
|
||||
|
||||
@@ -101,20 +100,46 @@ impl ChatSpace {
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
// Observe the client keys and show an alert modal if they fail to initialize
|
||||
cx.observe_in(&client_keys, window, |this, keys, window, cx| {
|
||||
if !keys.read(cx).has_keys() {
|
||||
this.render_client_keys_modal(window, cx);
|
||||
} else {
|
||||
this.load_local_account(window, cx);
|
||||
// Observe the keystore
|
||||
cx.observe_in(®istry, window, |this, registry, window, cx| {
|
||||
let has_keyring = registry.read(cx).initialized_keystore;
|
||||
let use_filestore = registry.read(cx).is_using_file_keystore();
|
||||
let not_logged_in = registry.read(cx).signer_pubkey().is_none();
|
||||
|
||||
if use_filestore && not_logged_in {
|
||||
this.render_keyring_installation(window, cx);
|
||||
}
|
||||
|
||||
if has_keyring && not_logged_in {
|
||||
let keystore = registry.read(cx).keystore();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let result = keystore
|
||||
.read_credentials(&KeyItem::User.to_string(), cx)
|
||||
.await;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
match result {
|
||||
Ok(Some((user, secret))) => {
|
||||
let public_key = PublicKey::parse(&user).unwrap();
|
||||
let secret = String::from_utf8(secret).unwrap();
|
||||
this.set_account_layout(public_key, secret, window, cx);
|
||||
}
|
||||
_ => {
|
||||
this.set_onboarding_layout(window, cx);
|
||||
}
|
||||
};
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
subscriptions.push(
|
||||
// Observe the global registry
|
||||
// Observe the global registry's events
|
||||
cx.observe_in(&status, window, move |this, status, window, cx| {
|
||||
let registry = Registry::global(cx);
|
||||
let status = status.read(cx);
|
||||
let all_panels = this.get_all_panel_ids(cx);
|
||||
|
||||
@@ -122,7 +147,7 @@ impl ChatSpace {
|
||||
status,
|
||||
UnwrappingStatus::Processing | UnwrappingStatus::Complete
|
||||
) {
|
||||
registry.update(cx, |this, cx| {
|
||||
Registry::global(cx).update(cx, |this, cx| {
|
||||
this.load_rooms(window, cx);
|
||||
this.refresh_rooms(all_panels, cx);
|
||||
});
|
||||
@@ -510,12 +535,12 @@ impl ChatSpace {
|
||||
|
||||
fn set_account_layout(
|
||||
&mut self,
|
||||
public_key: PublicKey,
|
||||
secret: String,
|
||||
profile: Profile,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let panel = Arc::new(account::init(profile, secret, window, cx));
|
||||
let panel = Arc::new(account::init(public_key, secret, window, cx));
|
||||
let center = DockItem::panel(panel);
|
||||
|
||||
self.dock.update(cx, |this, cx| {
|
||||
@@ -556,44 +581,6 @@ impl ChatSpace {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn load_local_account(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let task = cx.background_spawn(async move {
|
||||
let client = app_state().client();
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier(ACCOUNT_IDENTIFIER)
|
||||
.limit(1);
|
||||
|
||||
if let Some(event) = client.database().query(filter).await?.first_owned() {
|
||||
let metadata = client
|
||||
.database()
|
||||
.metadata(event.pubkey)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok((event.content, Profile::new(event.pubkey, metadata)))
|
||||
} else {
|
||||
Err(anyhow!("Empty"))
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Ok((secret, profile)) = task.await {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.set_account_layout(secret, profile, window, cx);
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.set_onboarding_layout(window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn on_settings(&mut self, _ev: &Settings, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let view = preferences::init(window, cx);
|
||||
|
||||
@@ -659,24 +646,7 @@ impl ChatSpace {
|
||||
}
|
||||
|
||||
fn on_sign_out(&mut self, _e: &Logout, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.background_spawn(async move {
|
||||
let states = app_state();
|
||||
let client = states.client();
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier(ACCOUNT_IDENTIFIER);
|
||||
|
||||
// Delete account
|
||||
client.database().delete(filter).await.ok();
|
||||
|
||||
// Reset the nostr client
|
||||
client.reset().await;
|
||||
|
||||
// Notify the channel about the signer being unset
|
||||
states.signal().send(SignalKind::SignerUnset).await;
|
||||
})
|
||||
.detach();
|
||||
reset(cx);
|
||||
}
|
||||
|
||||
fn on_open_pubkey(&mut self, ev: &OpenPublicKey, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -734,6 +704,32 @@ impl ChatSpace {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_keyring_installation(&mut self, window: &mut Window, cx: &mut App) {
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
this.overlay_closable(false)
|
||||
.show_close(false)
|
||||
.keyboard(false)
|
||||
.alert()
|
||||
.button_props(ModalButtonProps::default().ok_text(t!("common.continue")))
|
||||
.title(shared_t!("keyring_disable.label"))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.child(shared_t!("keyring_disable.body_1"))
|
||||
.child(shared_t!("keyring_disable.body_2"))
|
||||
.child(shared_t!("keyring_disable.body_3"))
|
||||
.child(shared_t!("keyring_disable.body_4"))
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().danger_foreground)
|
||||
.child(shared_t!("keyring_disable.body_5")),
|
||||
),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
fn render_setup_gossip_relays_modal(&mut self, window: &mut Window, cx: &mut App) {
|
||||
let relays = default_nip65_relays();
|
||||
|
||||
@@ -936,53 +932,6 @@ impl ChatSpace {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_client_keys_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
this.overlay_closable(false)
|
||||
.show_close(false)
|
||||
.keyboard(false)
|
||||
.confirm()
|
||||
.button_props(
|
||||
ModalButtonProps::default()
|
||||
.cancel_text(t!("startup.create_new_keys"))
|
||||
.ok_text(t!("common.allow")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.h_40()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.text_center()
|
||||
.text_sm()
|
||||
.child(
|
||||
div()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(shared_t!("startup.client_keys_warning")),
|
||||
)
|
||||
.child(shared_t!("startup.client_keys_desc")),
|
||||
)
|
||||
.on_cancel(|_, _window, cx| {
|
||||
ClientKeys::global(cx).update(cx, |this, cx| {
|
||||
this.new_keys(cx);
|
||||
});
|
||||
// true: Close modal
|
||||
true
|
||||
})
|
||||
.on_ok(|_, window, cx| {
|
||||
ClientKeys::global(cx).update(cx, |this, cx| {
|
||||
this.load(window, cx);
|
||||
});
|
||||
// true: Close modal
|
||||
true
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn render_titlebar_left_side(
|
||||
&mut self,
|
||||
_window: &mut Window,
|
||||
|
||||
@@ -83,9 +83,6 @@ fn main() {
|
||||
// Initialize components
|
||||
ui::init(cx);
|
||||
|
||||
// Initialize client keys
|
||||
client_keys::init(cx);
|
||||
|
||||
// Initialize app registry
|
||||
registry::init(cx);
|
||||
|
||||
|
||||
@@ -1,66 +1,68 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Error;
|
||||
use client_keys::ClientKeys;
|
||||
use common::display::RenderedProfile;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, relative, rems, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render,
|
||||
RetainAllImageCache, SharedString, StatefulInteractiveElement, Styled, Subscription, Task,
|
||||
WeakEntity, Window,
|
||||
Window,
|
||||
};
|
||||
use i18n::{shared_t, t};
|
||||
use nostr_connect::prelude::*;
|
||||
use nostr_sdk::prelude::*;
|
||||
use registry::keystore::KeyItem;
|
||||
use registry::Registry;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::app_state;
|
||||
use states::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT};
|
||||
use states::state::SignalKind;
|
||||
use states::constants::BUNKER_TIMEOUT;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::indicator::Indicator;
|
||||
use ui::input::{InputState, TextInput};
|
||||
use ui::notification::Notification;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{h_flex, v_flex, ContextModal, Sizable, StyledExt};
|
||||
|
||||
use crate::actions::CoopAuthUrlHandler;
|
||||
use crate::actions::{reset, CoopAuthUrlHandler};
|
||||
|
||||
pub fn init(
|
||||
profile: Profile,
|
||||
public_key: PublicKey,
|
||||
secret: String,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Account> {
|
||||
cx.new(|cx| Account::new(secret, profile, window, cx))
|
||||
cx.new(|cx| Account::new(public_key, secret, window, cx))
|
||||
}
|
||||
|
||||
pub struct Account {
|
||||
profile: Profile,
|
||||
stored_secret: String,
|
||||
is_bunker: bool,
|
||||
public_key: PublicKey,
|
||||
secret: String,
|
||||
loading: bool,
|
||||
|
||||
name: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
image_cache: Entity<RetainAllImageCache>,
|
||||
|
||||
/// Event subscriptions
|
||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||
|
||||
/// Background tasks
|
||||
_tasks: SmallVec<[Task<()>; 1]>,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
fn new(secret: String, profile: Profile, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let is_bunker = secret.starts_with("bunker://");
|
||||
fn new(
|
||||
public_key: PublicKey,
|
||||
secret: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let tasks = smallvec![];
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(
|
||||
// Clear the local state when user closes the account panel
|
||||
cx.on_release_in(window, move |this, window, cx| {
|
||||
this.stored_secret.clear();
|
||||
this.image_cache.update(cx, |this, cx| {
|
||||
this.clear(window, cx);
|
||||
});
|
||||
@@ -68,212 +70,116 @@ impl Account {
|
||||
);
|
||||
|
||||
Self {
|
||||
profile,
|
||||
is_bunker,
|
||||
stored_secret: secret,
|
||||
public_key,
|
||||
secret,
|
||||
loading: false,
|
||||
name: "Account".into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
image_cache: RetainAllImageCache::new(cx),
|
||||
_subscriptions: subscriptions,
|
||||
_tasks: smallvec![],
|
||||
_tasks: tasks,
|
||||
}
|
||||
}
|
||||
|
||||
fn login(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.set_loading(true, cx);
|
||||
|
||||
if self.is_bunker {
|
||||
if let Ok(uri) = NostrConnectURI::parse(&self.stored_secret) {
|
||||
self.nostr_connect(uri, window, cx);
|
||||
}
|
||||
} else if let Ok(enc) = EncryptedSecretKey::from_bech32(&self.stored_secret) {
|
||||
self.keys(enc, window, cx);
|
||||
} else {
|
||||
window.push_notification("Cannot continue with current account", cx);
|
||||
self.set_loading(false, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn nostr_connect(&mut self, uri: NostrConnectURI, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let client_keys = ClientKeys::global(cx);
|
||||
let app_keys = client_keys.read(cx).keys();
|
||||
|
||||
let timeout = Duration::from_secs(BUNKER_TIMEOUT);
|
||||
let mut signer = NostrConnect::new(uri, app_keys, timeout, None).unwrap();
|
||||
|
||||
// Handle auth url with the default browser
|
||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||
|
||||
self._tasks.push(
|
||||
// Handle connection in the background
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let client = app_state().client();
|
||||
|
||||
match signer.bunker_uri().await {
|
||||
Ok(_) => {
|
||||
// Set the client's signer with the current nostr connect instance
|
||||
client.set_signer(signer).await;
|
||||
}
|
||||
Err(e) => {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.set_loading(false, cx);
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
// Try to login with bunker
|
||||
if self.secret.starts_with("bunker://") {
|
||||
match NostrConnectURI::parse(&self.secret) {
|
||||
Ok(uri) => {
|
||||
self.login_with_bunker(uri, window, cx);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fn keys(&mut self, enc: EncryptedSecretKey, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let pwd_input: Entity<InputState> = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||
let weak_input = pwd_input.downgrade();
|
||||
|
||||
let error: Entity<Option<SharedString>> = cx.new(|_| None);
|
||||
let weak_error = error.downgrade();
|
||||
|
||||
let entity = cx.weak_entity();
|
||||
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
let entity = entity.clone();
|
||||
let entity_clone = entity.clone();
|
||||
let weak_input = weak_input.clone();
|
||||
let weak_error = weak_error.clone();
|
||||
|
||||
this.overlay_closable(false)
|
||||
.show_close(false)
|
||||
.keyboard(false)
|
||||
.confirm()
|
||||
.on_cancel(move |_, _window, cx| {
|
||||
entity
|
||||
.update(cx, |this, cx| {
|
||||
this.set_loading(false, cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
// true to close the modal
|
||||
true
|
||||
})
|
||||
.on_ok(move |_, window, cx| {
|
||||
let weak_error = weak_error.clone();
|
||||
let password = weak_input
|
||||
.read_with(cx, |state, _cx| state.value().to_owned())
|
||||
.ok();
|
||||
|
||||
entity_clone
|
||||
.update(cx, |this, cx| {
|
||||
this.verify_keys(enc, password, weak_error, window, cx);
|
||||
})
|
||||
.ok();
|
||||
|
||||
// false to keep the modal open
|
||||
false
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.child(shared_t!("login.password_to_decrypt"))
|
||||
.child(TextInput::new(&pwd_input).small())
|
||||
.when_some(error.read(cx).as_ref(), |this, error| {
|
||||
this.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.italic()
|
||||
.text_color(cx.theme().danger_foreground)
|
||||
.child(error.clone()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
fn verify_keys(
|
||||
&mut self,
|
||||
enc: EncryptedSecretKey,
|
||||
password: Option<SharedString>,
|
||||
error: WeakEntity<Option<SharedString>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(password) = password else {
|
||||
error
|
||||
.update(cx, |this, cx| {
|
||||
*this = Some("Password is required".into());
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
Err(e) => {
|
||||
window.push_notification(e.to_string(), cx);
|
||||
self.set_loading(false, cx);
|
||||
}
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
if password.is_empty() {
|
||||
error
|
||||
.update(cx, |this, cx| {
|
||||
*this = Some("Password cannot be empty".into());
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
return;
|
||||
// Fall back to login with keys
|
||||
match SecretKey::parse(&self.secret) {
|
||||
Ok(secret) => {
|
||||
self.login_with_keys(secret, cx);
|
||||
}
|
||||
Err(e) => {
|
||||
window.push_notification(e.to_string(), cx);
|
||||
self.set_loading(false, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let task: Task<Result<SecretKey, Error>> = cx.background_spawn(async move {
|
||||
let secret = enc.decrypt(&password)?;
|
||||
Ok(secret)
|
||||
});
|
||||
fn login_with_bunker(
|
||||
&mut self,
|
||||
uri: NostrConnectURI,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let keystore = Registry::global(cx).read(cx).keystore();
|
||||
|
||||
cx.spawn_in(window, async move |_this, cx| {
|
||||
match task.await {
|
||||
Ok(secret) => {
|
||||
cx.update(|window, cx| {
|
||||
window.close_all_modals(cx);
|
||||
})
|
||||
.ok();
|
||||
// Handle connection in the background
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let result = keystore
|
||||
.read_credentials(&KeyItem::Bunker.to_string(), cx)
|
||||
.await;
|
||||
|
||||
let client = app_state().client();
|
||||
let keys = Keys::new(secret);
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
match result {
|
||||
Ok(Some((_, content))) => {
|
||||
let secret = SecretKey::from_slice(&content).unwrap();
|
||||
let keys = Keys::new(secret);
|
||||
let timeout = Duration::from_secs(BUNKER_TIMEOUT);
|
||||
let mut signer = NostrConnect::new(uri, keys, timeout, None).unwrap();
|
||||
|
||||
// Set the client's signer with the current keys
|
||||
client.set_signer(keys).await
|
||||
}
|
||||
Err(e) => {
|
||||
error
|
||||
.update(cx, |this, cx| {
|
||||
*this = Some(e.to_string().into());
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
};
|
||||
// Handle auth url with the default browser
|
||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||
|
||||
// Connect to the remote signer
|
||||
this._tasks.push(
|
||||
// Handle connection in the background
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let client = app_state().client();
|
||||
|
||||
match signer.bunker_uri().await {
|
||||
Ok(_) => {
|
||||
client.set_signer(signer).await;
|
||||
}
|
||||
Err(e) => {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
window.push_notification(e.to_string(), cx);
|
||||
this.set_loading(false, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
Ok(None) => {
|
||||
window.push_notification(t!("login.keyring_required"), cx);
|
||||
this.set_loading(false, cx);
|
||||
}
|
||||
Err(e) => {
|
||||
window.push_notification(e.to_string(), cx);
|
||||
this.set_loading(false, cx);
|
||||
}
|
||||
};
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn logout(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self._tasks.push(
|
||||
// Reset the nostr client in the background
|
||||
cx.background_spawn(async move {
|
||||
let states = app_state();
|
||||
let client = states.client();
|
||||
fn login_with_keys(&mut self, secret: SecretKey, cx: &mut Context<Self>) {
|
||||
let keys = Keys::new(secret);
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::ApplicationSpecificData)
|
||||
.identifier(ACCOUNT_IDENTIFIER);
|
||||
|
||||
// Delete account
|
||||
client.database().delete(filter).await.ok();
|
||||
|
||||
// Unset the client's signer
|
||||
client.unset_signer().await;
|
||||
|
||||
// Notify the channel about the signer being unset
|
||||
states.signal().send(SignalKind::SignerUnset).await;
|
||||
}),
|
||||
);
|
||||
// Update the signer
|
||||
cx.background_spawn(async move {
|
||||
let client = app_state().client();
|
||||
client.set_signer(keys).await;
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
@@ -310,6 +216,10 @@ impl Focusable for Account {
|
||||
|
||||
impl Render for Account {
|
||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let registry = Registry::global(cx);
|
||||
let profile = registry.read(cx).get_person(&self.public_key, cx);
|
||||
let bunker = self.secret.starts_with("bunker://");
|
||||
|
||||
v_flex()
|
||||
.image_cache(self.image_cache.clone())
|
||||
.relative()
|
||||
@@ -367,8 +277,8 @@ impl Render for Account {
|
||||
)
|
||||
})
|
||||
.when(!self.loading, |this| {
|
||||
let avatar = self.profile.avatar(true);
|
||||
let name = self.profile.display_name();
|
||||
let avatar = profile.avatar(true);
|
||||
let name = profile.display_name();
|
||||
|
||||
this.child(
|
||||
h_flex()
|
||||
@@ -381,7 +291,7 @@ impl Render for Account {
|
||||
.child(Avatar::new(avatar).size(rems(1.5)))
|
||||
.child(div().pb_px().font_semibold().child(name)),
|
||||
)
|
||||
.child(div().when(self.is_bunker, |this| {
|
||||
.child(div().when(bunker, |this| {
|
||||
let label = SharedString::from("Nostr Connect");
|
||||
|
||||
this.child(
|
||||
@@ -407,9 +317,9 @@ impl Render for Account {
|
||||
Button::new("logout")
|
||||
.label(t!("user.sign_out"))
|
||||
.ghost()
|
||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||
this.logout(window, cx);
|
||||
})),
|
||||
.on_click(|_, _window, cx| {
|
||||
reset(cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ use std::fs;
|
||||
use std::time::Duration;
|
||||
|
||||
use dirs::document_dir;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, AppContext, ClipboardItem, Context, Entity, Flatten, IntoElement, ParentElement, Render,
|
||||
SharedString, Styled, Task, Window,
|
||||
@@ -15,7 +14,6 @@ use ui::input::{InputState, TextInput};
|
||||
use ui::{divider, h_flex, v_flex, Disableable, IconName, Sizable};
|
||||
|
||||
pub struct BackupKeys {
|
||||
password: Entity<InputState>,
|
||||
pubkey_input: Entity<InputState>,
|
||||
secret_input: Entity<InputState>,
|
||||
error: Option<SharedString>,
|
||||
@@ -27,8 +25,6 @@ impl BackupKeys {
|
||||
let Ok(npub) = keys.public_key.to_bech32();
|
||||
let Ok(nsec) = keys.secret_key().to_bech32();
|
||||
|
||||
let password = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||
|
||||
let pubkey_input = cx.new(|cx| {
|
||||
InputState::new(window, cx)
|
||||
.disabled(true)
|
||||
@@ -42,7 +38,6 @@ impl BackupKeys {
|
||||
});
|
||||
|
||||
Self {
|
||||
password,
|
||||
pubkey_input,
|
||||
secret_input,
|
||||
error: None,
|
||||
@@ -50,18 +45,8 @@ impl BackupKeys {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn password(&self, cx: &Context<Self>) -> String {
|
||||
self.password.read(cx).value().to_string()
|
||||
}
|
||||
|
||||
pub fn backup(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<Task<()>> {
|
||||
let document_dir = document_dir().expect("Failed to get document directory");
|
||||
let password = self.password.read(cx).value().to_string();
|
||||
|
||||
if password.is_empty() {
|
||||
self.set_error(t!("login.password_is_required"), window, cx);
|
||||
return None;
|
||||
};
|
||||
|
||||
let path = cx.prompt_for_new_path(&document_dir, Some("My Nostr Account"));
|
||||
let nsec = self.secret_input.read(cx).value().to_string();
|
||||
@@ -190,21 +175,5 @@ impl Render for BackupKeys {
|
||||
.child(shared_t!("new_account.backup_secret_note")),
|
||||
),
|
||||
)
|
||||
.child(divider(cx))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(shared_t!("login.set_password"))
|
||||
.child(TextInput::new(&self.password).small())
|
||||
.when_some(self.error.as_ref(), |this, error| {
|
||||
this.child(
|
||||
div()
|
||||
.italic()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().danger_foreground)
|
||||
.child(error.clone()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use client_keys::ClientKeys;
|
||||
use anyhow::anyhow;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Task,
|
||||
Window,
|
||||
Focusable, IntoElement, ParentElement, Render, SharedString, Styled, Subscription, Window,
|
||||
};
|
||||
use i18n::{shared_t, t};
|
||||
use nostr_connect::prelude::*;
|
||||
use registry::keystore::KeyItem;
|
||||
use registry::Registry;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::app_state;
|
||||
use states::constants::{ACCOUNT_IDENTIFIER, BUNKER_TIMEOUT};
|
||||
use states::constants::BUNKER_TIMEOUT;
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::notification::Notification;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{v_flex, ContextModal, Disableable, Sizable, StyledExt};
|
||||
use ui::{v_flex, ContextModal, Disableable, StyledExt};
|
||||
|
||||
use crate::actions::CoopAuthUrlHandler;
|
||||
|
||||
@@ -26,15 +28,19 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Login> {
|
||||
}
|
||||
|
||||
pub struct Login {
|
||||
input: Entity<InputState>,
|
||||
key_input: Entity<InputState>,
|
||||
pass_input: Entity<InputState>,
|
||||
error: Entity<Option<SharedString>>,
|
||||
countdown: Entity<Option<u64>>,
|
||||
require_password: bool,
|
||||
logging_in: bool,
|
||||
// Panel
|
||||
|
||||
/// Panel
|
||||
name: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
#[allow(unused)]
|
||||
subscriptions: SmallVec<[Subscription; 1]>,
|
||||
|
||||
/// Event subscriptions
|
||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
||||
}
|
||||
|
||||
impl Login {
|
||||
@@ -43,29 +49,42 @@ impl Login {
|
||||
}
|
||||
|
||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let input = cx.new(|cx| InputState::new(window, cx).placeholder("nsec... or bunker://..."));
|
||||
let key_input = cx.new(|cx| InputState::new(window, cx));
|
||||
let pass_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||
|
||||
let error = cx.new(|_| None);
|
||||
let countdown = cx.new(|_| None);
|
||||
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
// Subscribe to key input events and process login when the user presses enter
|
||||
subscriptions.push(
|
||||
cx.subscribe_in(&input, window, |this, _e, event, window, cx| {
|
||||
if let InputEvent::PressEnter { .. } = event {
|
||||
this.login(window, cx);
|
||||
}
|
||||
// Subscribe to key input events and process login when the user presses enter
|
||||
cx.subscribe_in(&key_input, window, |this, input, event, window, cx| {
|
||||
match event {
|
||||
InputEvent::PressEnter { .. } => {
|
||||
this.login(window, cx);
|
||||
}
|
||||
InputEvent::Change => {
|
||||
if input.read(cx).value().starts_with("ncryptsec1") {
|
||||
this.require_password = true;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
Self {
|
||||
input,
|
||||
key_input,
|
||||
pass_input,
|
||||
error,
|
||||
countdown,
|
||||
subscriptions,
|
||||
name: "Login".into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
logging_in: false,
|
||||
require_password: false,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,197 +96,34 @@ impl Login {
|
||||
// Prevent duplicate login requests
|
||||
self.set_logging_in(true, cx);
|
||||
|
||||
// Disable the input
|
||||
self.input.update(cx, |this, cx| {
|
||||
this.set_loading(true, cx);
|
||||
this.set_disabled(true, cx);
|
||||
});
|
||||
let value = self.key_input.read(cx).value();
|
||||
let password = self.pass_input.read(cx).value();
|
||||
|
||||
// Content can be secret key or bunker://
|
||||
match self.input.read(cx).value().to_string() {
|
||||
s if s.starts_with("nsec1") => self.ask_for_password(s, window, cx),
|
||||
s if s.starts_with("ncryptsec1") => self.ask_for_password(s, window, cx),
|
||||
s if s.starts_with("bunker://") => self.login_with_bunker(s, window, cx),
|
||||
_ => self.set_error(t!("login.invalid_key"), window, cx),
|
||||
};
|
||||
}
|
||||
|
||||
fn ask_for_password(&mut self, content: String, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let current_view = cx.entity().downgrade();
|
||||
let is_ncryptsec = content.starts_with("ncryptsec1");
|
||||
|
||||
let pwd_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||
let weak_pwd_input = pwd_input.downgrade();
|
||||
|
||||
let confirm_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||
let weak_confirm_input = confirm_input.downgrade();
|
||||
|
||||
window.open_modal(cx, move |this, _window, cx| {
|
||||
let weak_pwd_input = weak_pwd_input.clone();
|
||||
let weak_confirm_input = weak_confirm_input.clone();
|
||||
|
||||
let view_cancel = current_view.clone();
|
||||
let view_ok = current_view.clone();
|
||||
|
||||
let label: SharedString = if !is_ncryptsec {
|
||||
t!("login.set_password").into()
|
||||
} else {
|
||||
t!("login.password_to_decrypt").into()
|
||||
};
|
||||
|
||||
let description: SharedString = if is_ncryptsec {
|
||||
t!("login.password_description").into()
|
||||
} else {
|
||||
t!("login.password_description_full").into()
|
||||
};
|
||||
|
||||
this.overlay_closable(false)
|
||||
.show_close(false)
|
||||
.keyboard(false)
|
||||
.confirm()
|
||||
.on_cancel(move |_, window, cx| {
|
||||
view_cancel
|
||||
.update(cx, |this, cx| {
|
||||
this.set_error(t!("login.password_is_required"), window, cx);
|
||||
})
|
||||
.ok();
|
||||
true
|
||||
})
|
||||
.on_ok(move |_, window, cx| {
|
||||
let value = weak_pwd_input
|
||||
.read_with(cx, |state, _cx| state.value().to_owned())
|
||||
.ok();
|
||||
|
||||
let confirm = weak_confirm_input
|
||||
.read_with(cx, |state, _cx| state.value().to_owned())
|
||||
.ok();
|
||||
|
||||
view_ok
|
||||
.update(cx, |this, cx| {
|
||||
this.verify_password(value, confirm, is_ncryptsec, window, cx);
|
||||
})
|
||||
.ok();
|
||||
true
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.text_sm()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(label)
|
||||
.child(TextInput::new(&pwd_input).small()),
|
||||
)
|
||||
.when(content.starts_with("nsec1"), |this| {
|
||||
this.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.child(SharedString::new(t!("login.confirm_password")))
|
||||
.child(TextInput::new(&confirm_input).small()),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.italic()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.child(description),
|
||||
),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
fn verify_password(
|
||||
&mut self,
|
||||
password: Option<SharedString>,
|
||||
confirm: Option<SharedString>,
|
||||
is_ncryptsec: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(password) = password else {
|
||||
self.set_error(t!("login.password_is_required"), window, cx);
|
||||
return;
|
||||
};
|
||||
|
||||
if password.is_empty() {
|
||||
self.set_error(t!("login.password_is_required"), window, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip verification if key is ncryptsec
|
||||
if is_ncryptsec {
|
||||
self.login_with_keys(password.to_string(), window, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(confirm) = confirm else {
|
||||
self.set_error(t!("login.must_confirm_password"), window, cx);
|
||||
return;
|
||||
};
|
||||
|
||||
if confirm.is_empty() {
|
||||
self.set_error(t!("login.must_confirm_password"), window, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
if password != confirm {
|
||||
self.set_error(t!("login.password_not_match"), window, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
self.login_with_keys(password.to_string(), window, cx);
|
||||
}
|
||||
|
||||
fn login_with_keys(&mut self, password: String, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let value = self.input.read(cx).value().to_string();
|
||||
|
||||
let secret_key = if value.starts_with("nsec1") {
|
||||
SecretKey::parse(&value).ok()
|
||||
if value.starts_with("bunker://") {
|
||||
self.login_with_bunker(&value, window, cx);
|
||||
} else if value.starts_with("ncryptsec1") {
|
||||
EncryptedSecretKey::from_bech32(&value)
|
||||
.map(|enc| enc.decrypt(&password).ok())
|
||||
.unwrap_or_default()
|
||||
self.login_with_password(&value, &password, cx);
|
||||
} else if value.starts_with("nsec1") {
|
||||
if let Ok(secret) = SecretKey::parse(&value) {
|
||||
let keys = Keys::new(secret);
|
||||
self.login_with_keys(keys, cx);
|
||||
} else {
|
||||
self.set_error("Invalid", cx);
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(secret_key) = secret_key {
|
||||
let keys = Keys::new(secret_key);
|
||||
|
||||
// Encrypt and save user secret key to disk
|
||||
self.write_keys_to_disk(&keys, password, cx);
|
||||
|
||||
// Set the client's signer with the current keys
|
||||
cx.background_spawn(async move {
|
||||
let client = app_state().client();
|
||||
client.set_signer(keys).await;
|
||||
})
|
||||
.detach();
|
||||
} else {
|
||||
self.set_error(t!("login.key_invalid"), window, cx);
|
||||
self.set_error("Invalid", cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn login_with_bunker(&mut self, content: String, window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn login_with_bunker(&mut self, content: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Ok(uri) = NostrConnectURI::parse(content) else {
|
||||
self.set_error(t!("login.bunker_invalid"), window, cx);
|
||||
self.set_error(t!("login.bunker_invalid"), cx);
|
||||
return;
|
||||
};
|
||||
|
||||
let client_keys = ClientKeys::global(cx);
|
||||
let app_keys = client_keys.read(cx).keys();
|
||||
|
||||
let app_keys = Keys::generate();
|
||||
let timeout = Duration::from_secs(BUNKER_TIMEOUT);
|
||||
let mut signer = NostrConnect::new(uri, app_keys, timeout, None).unwrap();
|
||||
let mut signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap();
|
||||
|
||||
// Handle auth url with the default browser
|
||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||
@@ -293,103 +149,152 @@ impl Login {
|
||||
|
||||
// Handle connection
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
match signer.bunker_uri().await {
|
||||
Ok(uri) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.write_uri_to_disk(signer, uri, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(error) => {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.set_error(error.to_string(), window, cx);
|
||||
// Force reset the client keys
|
||||
//
|
||||
// This step is necessary to ensure that user can retry the connection
|
||||
client_keys.update(cx, |this, cx| {
|
||||
this.force_new_keys(cx);
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
let result = signer.bunker_uri().await;
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
match result {
|
||||
Ok(uri) => {
|
||||
this.save_connection(&app_keys, &uri, window, cx);
|
||||
this.connect(signer, cx);
|
||||
}
|
||||
Err(e) => {
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
}
|
||||
};
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn write_uri_to_disk(
|
||||
fn save_connection(
|
||||
&mut self,
|
||||
signer: NostrConnect,
|
||||
uri: NostrConnectURI,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut uri_without_secret = uri.to_string();
|
||||
|
||||
// Clear the secret parameter in the URI if it exists
|
||||
if let Some(secret) = uri.secret() {
|
||||
uri_without_secret = uri_without_secret.replace(secret, "");
|
||||
}
|
||||
|
||||
let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
|
||||
let client = app_state().client();
|
||||
|
||||
// Update the client's signer
|
||||
client.set_signer(signer).await;
|
||||
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
let event = EventBuilder::new(Kind::ApplicationSpecificData, uri_without_secret)
|
||||
.tags(vec![Tag::identifier(ACCOUNT_IDENTIFIER)])
|
||||
.build(public_key)
|
||||
.sign(&Keys::generate())
|
||||
.await?;
|
||||
|
||||
// Save the event to the database
|
||||
client.database().save_event(&event).await?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
task.detach();
|
||||
}
|
||||
|
||||
pub fn write_keys_to_disk(&self, keys: &Keys, password: String, cx: &mut Context<Self>) {
|
||||
let keys = keys.to_owned();
|
||||
let public_key = keys.public_key();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
if let Ok(enc_key) =
|
||||
EncryptedSecretKey::new(keys.secret_key(), &password, 8, KeySecurity::Unknown)
|
||||
{
|
||||
let client = app_state().client();
|
||||
let value = enc_key.to_bech32().unwrap();
|
||||
let keys = Keys::generate();
|
||||
let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)];
|
||||
let kind = Kind::ApplicationSpecificData;
|
||||
|
||||
let builder = EventBuilder::new(kind, value)
|
||||
.tags(tags)
|
||||
.build(public_key)
|
||||
.sign(&keys)
|
||||
.await;
|
||||
|
||||
if let Ok(event) = builder {
|
||||
if let Err(e) = client.database().save_event(&event).await {
|
||||
log::error!("Failed to save event: {e}");
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn set_error(
|
||||
&mut self,
|
||||
message: impl Into<SharedString>,
|
||||
keys: &Keys,
|
||||
uri: &NostrConnectURI,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let keystore = Registry::global(cx).read(cx).keystore();
|
||||
let username = keys.public_key().to_hex();
|
||||
let secret = keys.secret_key().to_secret_bytes();
|
||||
let mut clean_uri = uri.to_string();
|
||||
|
||||
// Clear the secret parameter in the URI if it exists
|
||||
if let Some(s) = uri.secret() {
|
||||
clean_uri = clean_uri.replace(s, "");
|
||||
}
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let user_url = KeyItem::User.to_string();
|
||||
let bunker_url = KeyItem::Bunker.to_string();
|
||||
let user_password = clean_uri.into_bytes();
|
||||
|
||||
// Write bunker uri to keyring for further connection
|
||||
if let Err(e) = keystore
|
||||
.write_credentials(&user_url, "bunker", &user_password, cx)
|
||||
.await
|
||||
{
|
||||
this.update_in(cx, |_, window, cx| {
|
||||
window.push_notification(e.to_string(), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Write the app keys for further connection
|
||||
if let Err(e) = keystore
|
||||
.write_credentials(&bunker_url, &username, &secret, cx)
|
||||
.await
|
||||
{
|
||||
this.update_in(cx, |_, window, cx| {
|
||||
window.push_notification(e.to_string(), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn connect(&mut self, signer: NostrConnect, cx: &mut Context<Self>) {
|
||||
cx.background_spawn(async move {
|
||||
let client = app_state().client();
|
||||
client.set_signer(signer).await;
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn login_with_password(&mut self, content: &str, pwd: &str, cx: &mut Context<Self>) {
|
||||
if pwd.is_empty() {
|
||||
self.set_error("Password is required", cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let Ok(enc) = EncryptedSecretKey::from_bech32(content) else {
|
||||
self.set_error("Secret Key is invalid", cx);
|
||||
return;
|
||||
};
|
||||
|
||||
let password = pwd.to_owned();
|
||||
|
||||
// Decrypt in the background to ensure it doesn't block the UI
|
||||
let task = cx.background_spawn(async move {
|
||||
if let Ok(content) = enc.decrypt(&password) {
|
||||
Ok(Keys::new(content))
|
||||
} else {
|
||||
Err(anyhow!("Invalid password"))
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let result = task.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
match result {
|
||||
Ok(keys) => {
|
||||
this.login_with_keys(keys, cx);
|
||||
}
|
||||
Err(e) => {
|
||||
this.set_error(e.to_string(), cx);
|
||||
}
|
||||
};
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn login_with_keys(&mut self, keys: Keys, cx: &mut Context<Self>) {
|
||||
let keystore = Registry::global(cx).read(cx).keystore();
|
||||
let username = keys.public_key().to_hex();
|
||||
let secret = keys.secret_key().to_secret_hex().into_bytes();
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
let bunker_url = KeyItem::User.to_string();
|
||||
|
||||
// Write the app keys for further connection
|
||||
if let Err(e) = keystore
|
||||
.write_credentials(&bunker_url, &username, &secret, cx)
|
||||
.await
|
||||
{
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_error(e.to_string(), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Update the signer
|
||||
cx.background_spawn(async move {
|
||||
let client = app_state().client();
|
||||
client.set_signer(keys).await;
|
||||
})
|
||||
.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn set_error<S>(&mut self, message: S, cx: &mut Context<Self>)
|
||||
where
|
||||
S: Into<SharedString>,
|
||||
{
|
||||
// Reset the log in state
|
||||
self.set_logging_in(false, cx);
|
||||
|
||||
@@ -402,13 +307,6 @@ impl Login {
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
// Re enable the input
|
||||
self.input.update(cx, |this, cx| {
|
||||
this.set_value("", window, cx);
|
||||
this.set_loading(false, cx);
|
||||
this.set_disabled(false, cx);
|
||||
});
|
||||
|
||||
// Clear the error message after 3 secs
|
||||
cx.spawn(async move |this, cx| {
|
||||
cx.background_executor().timer(Duration::from_secs(3)).await;
|
||||
@@ -493,7 +391,25 @@ impl Render for Login {
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.child(TextInput::new(&self.input))
|
||||
.text_sm()
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child("nsec or bunker://")
|
||||
.child(TextInput::new(&self.key_input)),
|
||||
)
|
||||
.when(self.require_password, |this| {
|
||||
this.child(
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child("Password:")
|
||||
.child(TextInput::new(&self.pass_input)),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Button::new("login")
|
||||
.label(t!("common.continue"))
|
||||
@@ -513,13 +429,13 @@ impl Render for Login {
|
||||
.child(shared_t!("login.approve_message", i = i)),
|
||||
)
|
||||
})
|
||||
.when_some(self.error.read(cx).clone(), |this, error| {
|
||||
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
||||
this.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.text_color(cx.theme().danger_foreground)
|
||||
.child(error),
|
||||
.child(error.clone()),
|
||||
)
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -8,9 +8,11 @@ use gpui::{
|
||||
use gpui_tokio::Tokio;
|
||||
use i18n::{shared_t, t};
|
||||
use nostr_sdk::prelude::*;
|
||||
use registry::keystore::KeyItem;
|
||||
use registry::Registry;
|
||||
use settings::AppSettings;
|
||||
use smol::fs;
|
||||
use states::constants::{ACCOUNT_IDENTIFIER, BOOTSTRAP_RELAYS};
|
||||
use states::constants::BOOTSTRAP_RELAYS;
|
||||
use states::{app_state, default_nip17_relays, default_nip65_relays};
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
@@ -81,21 +83,17 @@ impl NewAccount {
|
||||
.on_ok(move |_, window, cx| {
|
||||
weak_view
|
||||
.update(cx, |this, cx| {
|
||||
let password = this.password(cx);
|
||||
let current_view = current_view.clone();
|
||||
|
||||
if let Some(task) = this.backup(window, cx) {
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
task.await;
|
||||
|
||||
cx.update(|window, cx| {
|
||||
current_view
|
||||
.update(cx, |this, cx| {
|
||||
this.set_signer(password, window, cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.ok()
|
||||
current_view
|
||||
.update(cx, |this, cx| {
|
||||
this.set_signer(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -107,10 +105,13 @@ impl NewAccount {
|
||||
})
|
||||
}
|
||||
|
||||
fn set_signer(&mut self, password: String, window: &mut Window, cx: &mut Context<Self>) {
|
||||
window.close_modal(cx);
|
||||
pub fn set_signer(&mut self, cx: &mut Context<Self>) {
|
||||
let keystore = Registry::global(cx).read(cx).keystore();
|
||||
|
||||
let keys = self.temp_keys.read(cx).clone();
|
||||
let username = keys.public_key().to_hex();
|
||||
let secret = keys.secret_key().to_secret_hex().into_bytes();
|
||||
|
||||
let avatar = self.avatar_input.read(cx).value().to_string();
|
||||
let name = self.name_input.read(cx).value().to_string();
|
||||
let mut metadata = Metadata::new().display_name(name.clone()).name(name);
|
||||
@@ -119,81 +120,59 @@ impl NewAccount {
|
||||
metadata = metadata.picture(url);
|
||||
};
|
||||
|
||||
// Encrypt and save user secret key to disk
|
||||
self.write_keys_to_disk(&keys, password, cx);
|
||||
cx.spawn(async move |_, cx| {
|
||||
let url = KeyItem::User.to_string();
|
||||
|
||||
// Set the client's signer with the current keys
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let client = app_state().client();
|
||||
// Write the app keys for further connection
|
||||
keystore
|
||||
.write_credentials(&url, &username, &secret, cx)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
// Update the signer
|
||||
// Set the client's signer with the current keys
|
||||
client.set_signer(keys).await;
|
||||
|
||||
// Verify the signer
|
||||
let signer = client.signer().await?;
|
||||
|
||||
// Construct a NIP-65 event
|
||||
let event = EventBuilder::new(Kind::RelayList, "")
|
||||
.tags(default_nip65_relays().iter().map(|(url, metadata)| {
|
||||
Tag::relay_metadata(url.to_owned(), metadata.to_owned())
|
||||
}))
|
||||
.sign(&signer)
|
||||
.await?;
|
||||
|
||||
// Set NIP-65 relays
|
||||
client.send_event_to(BOOTSTRAP_RELAYS, &event).await?;
|
||||
|
||||
// Construct a NIP-17 event
|
||||
let event = EventBuilder::new(Kind::InboxRelays, "")
|
||||
.tags(
|
||||
default_nip17_relays()
|
||||
.iter()
|
||||
.map(|url| Tag::relay(url.to_owned())),
|
||||
)
|
||||
.sign(&signer)
|
||||
.await?;
|
||||
|
||||
// Set NIP-17 relays
|
||||
client.send_event(&event).await?;
|
||||
|
||||
// Construct a metadata event
|
||||
let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
|
||||
|
||||
// Set metadata
|
||||
client.send_event(&event).await?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
task.detach();
|
||||
}
|
||||
|
||||
fn write_keys_to_disk(&self, keys: &Keys, password: String, cx: &mut Context<Self>) {
|
||||
let keys = keys.to_owned();
|
||||
let public_key = keys.public_key();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
if let Ok(enc_key) =
|
||||
EncryptedSecretKey::new(keys.secret_key(), &password, 8, KeySecurity::Unknown)
|
||||
{
|
||||
let task: Task<Result<(), Error>> = cx.background_spawn(async move {
|
||||
let client = app_state().client();
|
||||
let value = enc_key.to_bech32().unwrap();
|
||||
let keys = Keys::generate();
|
||||
let tags = vec![Tag::identifier(ACCOUNT_IDENTIFIER)];
|
||||
let kind = Kind::ApplicationSpecificData;
|
||||
|
||||
let builder = EventBuilder::new(kind, value)
|
||||
.tags(tags)
|
||||
.build(public_key)
|
||||
.sign(&keys)
|
||||
.await;
|
||||
// Set the client's signer with the current keys
|
||||
client.set_signer(keys).await;
|
||||
|
||||
if let Ok(event) = builder {
|
||||
if let Err(e) = client.database().save_event(&event).await {
|
||||
log::error!("Failed to save event: {e}");
|
||||
};
|
||||
}
|
||||
}
|
||||
// Verify the signer
|
||||
let signer = client.signer().await?;
|
||||
|
||||
// Construct a NIP-65 event
|
||||
let event = EventBuilder::new(Kind::RelayList, "")
|
||||
.tags(
|
||||
default_nip65_relays()
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|(url, metadata)| Tag::relay_metadata(url, metadata)),
|
||||
)
|
||||
.sign(&signer)
|
||||
.await?;
|
||||
|
||||
// Set NIP-65 relays
|
||||
client.send_event_to(BOOTSTRAP_RELAYS, &event).await?;
|
||||
|
||||
// Construct a NIP-17 event
|
||||
let event = EventBuilder::new(Kind::InboxRelays, "")
|
||||
.tags(default_nip17_relays().iter().cloned().map(Tag::relay))
|
||||
.sign(&signer)
|
||||
.await?;
|
||||
|
||||
// Set NIP-17 relays
|
||||
client.send_event(&event).await?;
|
||||
|
||||
// Construct a metadata event
|
||||
let event = EventBuilder::metadata(&metadata).sign(&signer).await?;
|
||||
|
||||
// Set metadata
|
||||
client.send_event(&event).await?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
task.detach();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use client_keys::ClientKeys;
|
||||
use common::display::TextUtils;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, img, px, relative, svg, AnyElement, App, AppContext, ClipboardItem, Context, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement,
|
||||
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window,
|
||||
div, img, px, relative, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement, Render,
|
||||
SharedString, StatefulInteractiveElement, Styled, Task, Window,
|
||||
};
|
||||
use i18n::{shared_t, t};
|
||||
use nostr_connect::prelude::*;
|
||||
use registry::keystore::KeyItem;
|
||||
use registry::Registry;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use states::app_state;
|
||||
use states::constants::{ACCOUNT_IDENTIFIER, APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT};
|
||||
use states::constants::{APP_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT};
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
||||
@@ -21,7 +22,7 @@ use ui::notification::Notification;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::{divider, h_flex, v_flex, ContextModal, Icon, IconName, Sizable, StyledExt};
|
||||
|
||||
use crate::chatspace;
|
||||
use crate::chatspace::{self};
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
|
||||
Onboarding::new(window, cx)
|
||||
@@ -59,14 +60,14 @@ impl NostrConnectApp {
|
||||
}
|
||||
|
||||
pub struct Onboarding {
|
||||
nostr_connect_uri: Entity<NostrConnectURI>,
|
||||
nostr_connect: Entity<Option<NostrConnect>>,
|
||||
qr_code: Entity<Option<Arc<Image>>>,
|
||||
connecting: bool,
|
||||
// Panel
|
||||
app_keys: Keys,
|
||||
qr_code: Option<Arc<Image>>,
|
||||
|
||||
/// Panel
|
||||
name: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
_subscriptions: SmallVec<[Subscription; 2]>,
|
||||
|
||||
/// Background tasks
|
||||
_tasks: SmallVec<[Task<()>; 1]>,
|
||||
}
|
||||
|
||||
@@ -76,145 +77,101 @@ impl Onboarding {
|
||||
}
|
||||
|
||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let nostr_connect = cx.new(|_| None);
|
||||
let qr_code = cx.new(|_| None);
|
||||
let app_keys = Keys::generate();
|
||||
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||
|
||||
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
|
||||
let uri = NostrConnectURI::client(app_keys.public_key(), vec![relay], APP_NAME);
|
||||
let qr_code = uri.to_string().to_qr();
|
||||
|
||||
// NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md
|
||||
//
|
||||
// Direct connection initiated by the client
|
||||
let nostr_connect_uri = cx.new(|cx| {
|
||||
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
|
||||
let app_keys = ClientKeys::read_global(cx).keys();
|
||||
NostrConnectURI::client(app_keys.public_key(), vec![relay], APP_NAME)
|
||||
});
|
||||
let signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap();
|
||||
|
||||
let mut subscriptions = smallvec![];
|
||||
let mut tasks = smallvec![];
|
||||
|
||||
// Clean up when the current view is released
|
||||
subscriptions.push(cx.on_release_in(window, |this, window, cx| {
|
||||
this.shutdown_nostr_connect(window, cx);
|
||||
}));
|
||||
|
||||
// Set Nostr Connect after the view is initialized
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
this.set_connect(window, cx);
|
||||
});
|
||||
|
||||
Self {
|
||||
nostr_connect,
|
||||
nostr_connect_uri,
|
||||
qr_code,
|
||||
connecting: false,
|
||||
name: "Onboarding".into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
_subscriptions: subscriptions,
|
||||
_tasks: smallvec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn set_connecting(&mut self, cx: &mut Context<Self>) {
|
||||
self.connecting = true;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn set_connect(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let uri = self.nostr_connect_uri.read(cx).clone();
|
||||
let app_keys = ClientKeys::read_global(cx).keys();
|
||||
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||
|
||||
self.qr_code.update(cx, |this, cx| {
|
||||
*this = uri.to_string().to_qr();
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
self.nostr_connect.update(cx, |this, cx| {
|
||||
*this = NostrConnect::new(uri, app_keys, timeout, None).ok();
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
self._tasks.push(
|
||||
// Wait for Nostr Connect approval
|
||||
tasks.push(
|
||||
// Wait for nostr connect
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let connect = this.read_with(cx, |this, cx| this.nostr_connect.read(cx).clone());
|
||||
let result = signer.bunker_uri().await;
|
||||
|
||||
if let Ok(Some(signer)) = connect {
|
||||
match signer.bunker_uri().await {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
match result {
|
||||
Ok(uri) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.set_connecting(cx);
|
||||
this.write_uri_to_disk(signer, uri, cx);
|
||||
})
|
||||
.ok();
|
||||
this.save_connection(&uri, window, cx);
|
||||
this.connect(signer, cx);
|
||||
}
|
||||
Err(e) => {
|
||||
this.update_in(cx, |_, window, cx| {
|
||||
window.push_notification(
|
||||
Notification::error(e.to_string()).title("Nostr Connect"),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
window.push_notification(Notification::error(e.to_string()), cx);
|
||||
}
|
||||
};
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}),
|
||||
)
|
||||
);
|
||||
|
||||
Self {
|
||||
qr_code,
|
||||
app_keys,
|
||||
name: "Onboarding".into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
_tasks: tasks,
|
||||
}
|
||||
}
|
||||
|
||||
fn write_uri_to_disk(
|
||||
fn save_connection(
|
||||
&mut self,
|
||||
signer: NostrConnect,
|
||||
uri: NostrConnectURI,
|
||||
uri: &NostrConnectURI,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut uri_without_secret = uri.to_string();
|
||||
let keystore = Registry::global(cx).read(cx).keystore();
|
||||
let username = self.app_keys.public_key().to_hex();
|
||||
let secret = self.app_keys.secret_key().to_secret_bytes();
|
||||
let mut clean_uri = uri.to_string();
|
||||
|
||||
// Clear the secret parameter in the URI if it exists
|
||||
if let Some(secret) = uri.secret() {
|
||||
uri_without_secret = uri_without_secret.replace(secret, "");
|
||||
if let Some(s) = uri.secret() {
|
||||
clean_uri = clean_uri.replace(s, "");
|
||||
}
|
||||
|
||||
let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
|
||||
let client = app_state().client();
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let user_url = KeyItem::User.to_string();
|
||||
let bunker_url = KeyItem::Bunker.to_string();
|
||||
let user_password = clean_uri.into_bytes();
|
||||
|
||||
// Update the client's signer
|
||||
client.set_signer(signer).await;
|
||||
|
||||
let signer = client.signer().await?;
|
||||
let public_key = signer.get_public_key().await?;
|
||||
|
||||
let event = EventBuilder::new(Kind::ApplicationSpecificData, uri_without_secret)
|
||||
.tags(vec![Tag::identifier(ACCOUNT_IDENTIFIER)])
|
||||
.build(public_key)
|
||||
.sign(&Keys::generate())
|
||||
.await?;
|
||||
|
||||
// Save the event to the database
|
||||
client.database().save_event(&event).await?;
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
task.detach();
|
||||
}
|
||||
|
||||
fn copy_uri(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
self.nostr_connect_uri.read(cx).to_string(),
|
||||
));
|
||||
window.push_notification(t!("common.copied"), cx);
|
||||
}
|
||||
|
||||
fn shutdown_nostr_connect(&mut self, _window: &mut Window, cx: &mut App) {
|
||||
if !self.connecting {
|
||||
if let Some(signer) = self.nostr_connect.read(cx).clone() {
|
||||
cx.background_spawn(async move {
|
||||
log::info!("Shutting down Nostr Connect");
|
||||
signer.shutdown().await;
|
||||
// Write bunker uri to keyring for further connection
|
||||
if let Err(e) = keystore
|
||||
.write_credentials(&user_url, "bunker", &user_password, cx)
|
||||
.await
|
||||
{
|
||||
this.update_in(cx, |_, window, cx| {
|
||||
window.push_notification(e.to_string(), cx);
|
||||
})
|
||||
.detach();
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
// Write the app keys for further connection
|
||||
if let Err(e) = keystore
|
||||
.write_credentials(&bunker_url, &username, &secret, cx)
|
||||
.await
|
||||
{
|
||||
this.update_in(cx, |_, window, cx| {
|
||||
window.push_notification(e.to_string(), cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn connect(&mut self, signer: NostrConnect, cx: &mut Context<Self>) {
|
||||
cx.background_spawn(async move {
|
||||
let client = app_state().client();
|
||||
client.set_signer(signer).await;
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn render_apps(&self, cx: &Context<Self>) -> impl IntoIterator<Item = impl IntoElement> {
|
||||
@@ -368,23 +325,14 @@ impl Render for Onboarding {
|
||||
.gap_5()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.when_some(self.qr_code.read(cx).as_ref(), |this, qr| {
|
||||
.when_some(self.qr_code.as_ref(), |this, qr| {
|
||||
this.child(
|
||||
div()
|
||||
.id("")
|
||||
.child(
|
||||
img(qr.clone())
|
||||
.size(px(256.))
|
||||
.rounded_xl()
|
||||
.shadow_lg()
|
||||
.border_1()
|
||||
.border_color(cx.theme().element_active),
|
||||
)
|
||||
.on_click(cx.listener(
|
||||
move |this, _e, window, cx| {
|
||||
this.copy_uri(window, cx)
|
||||
},
|
||||
)),
|
||||
img(qr.clone())
|
||||
.size(px(256.))
|
||||
.rounded_xl()
|
||||
.shadow_lg()
|
||||
.border_1()
|
||||
.border_color(cx.theme().element_active),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
|
||||
Reference in New Issue
Block a user