feat: support nip46
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
use app_state::registry::AppRegistry;
|
||||
use common::profile::NostrProfile;
|
||||
use common::{profile::NostrProfile, qr::create_qr};
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder, relative, svg, App, AppContext, BorrowAppContext, Context, Entity,
|
||||
IntoElement, ParentElement, Render, Styled, Window,
|
||||
div, img, prelude::FluentBuilder, relative, svg, App, AppContext, BorrowAppContext,
|
||||
ClipboardItem, Context, Div, Entity, IntoElement, ParentElement, Render, Styled, Window,
|
||||
};
|
||||
use nostr_connect::prelude::*;
|
||||
use state::get_client;
|
||||
use std::time::Duration;
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
use tokio::sync::oneshot;
|
||||
use ui::{
|
||||
button::{Button, ButtonVariants},
|
||||
button::{Button, ButtonCustomVariant, ButtonVariants},
|
||||
input::{InputEvent, TextInput},
|
||||
notification::NotificationType,
|
||||
theme::{scale::ColorScaleStep, ActiveTheme},
|
||||
@@ -18,7 +18,7 @@ use ui::{
|
||||
|
||||
use super::app;
|
||||
|
||||
const ALPHA_MESSAGE: &str = "Coop is in the alpha stage; it does not store any credentials. You will need to log in again when you reopen the app.";
|
||||
const ALPHA_MESSAGE: &str = "Coop is in the alpha stage; it doesn't store any credentials. You will need to log in again when you relanch.";
|
||||
const JOIN_URL: &str = "https://start.njump.me/";
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
|
||||
@@ -26,7 +26,10 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
|
||||
}
|
||||
|
||||
pub struct Onboarding {
|
||||
input: Entity<TextInput>,
|
||||
app_keys: Keys,
|
||||
connect_uri: NostrConnectURI,
|
||||
qr_path: Option<PathBuf>,
|
||||
nsec_input: Entity<TextInput>,
|
||||
use_connect: bool,
|
||||
use_privkey: bool,
|
||||
is_loading: bool,
|
||||
@@ -34,26 +37,41 @@ pub struct Onboarding {
|
||||
|
||||
impl Onboarding {
|
||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
let input = cx.new(|cx| {
|
||||
let app_keys = Keys::generate();
|
||||
|
||||
let connect_uri = NostrConnectURI::client(
|
||||
app_keys.public_key(),
|
||||
vec![RelayUrl::parse("wss://relay.nsec.app").unwrap()],
|
||||
"Coop",
|
||||
);
|
||||
|
||||
let nsec_input = cx.new(|cx| {
|
||||
TextInput::new(window, cx)
|
||||
.text_size(Size::XSmall)
|
||||
.placeholder("nsec...")
|
||||
});
|
||||
|
||||
// Save Connect URI as PNG file for display as QR Code
|
||||
let qr_path = create_qr(connect_uri.to_string().as_str()).ok();
|
||||
|
||||
cx.new(|cx| {
|
||||
// Handle Enter event for nsec input
|
||||
cx.subscribe_in(
|
||||
&input,
|
||||
&nsec_input,
|
||||
window,
|
||||
move |this: &mut Self, _, input_event, window, cx| {
|
||||
if let InputEvent::PressEnter = input_event {
|
||||
this.login(window, cx);
|
||||
this.privkey_login(window, cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
input,
|
||||
app_keys,
|
||||
connect_uri,
|
||||
qr_path,
|
||||
nsec_input,
|
||||
use_connect: false,
|
||||
use_privkey: false,
|
||||
is_loading: false,
|
||||
@@ -61,12 +79,49 @@ impl Onboarding {
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
fn use_connect(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn use_connect(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let uri = self.connect_uri.clone();
|
||||
let app_keys = self.app_keys.clone();
|
||||
let window_handle = window.window_handle();
|
||||
|
||||
self.use_connect = true;
|
||||
cx.notify();
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let (tx, rx) = oneshot::channel::<NostrProfile>();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
if let Ok(signer) = NostrConnect::new(uri, app_keys, Duration::from_secs(300), None)
|
||||
{
|
||||
if let Ok(uri) = signer.bunker_uri().await {
|
||||
let client = get_client();
|
||||
|
||||
if let Some(public_key) = uri.remote_signer_public_key() {
|
||||
let metadata = client
|
||||
.fetch_metadata(*public_key, Duration::from_secs(2))
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or_default();
|
||||
|
||||
_ = client.set_signer(signer).await;
|
||||
_ = tx.send(NostrProfile::new(*public_key, metadata));
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
if let Ok(profile) = rx.await {
|
||||
_ = cx.update_window(window_handle, |_, window, cx| {
|
||||
cx.update_global::<AppRegistry, _>(|this, cx| {
|
||||
this.set_user(Some(profile.clone()));
|
||||
this.set_root_view(app::init(profile, window, cx).into(), cx);
|
||||
});
|
||||
})
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
*/
|
||||
|
||||
fn use_privkey(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.use_privkey = true;
|
||||
@@ -84,60 +139,212 @@ impl Onboarding {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn login(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let value = self.input.read(cx).text().to_string();
|
||||
fn privkey_login(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let value = self.nsec_input.read(cx).text().to_string();
|
||||
let window_handle = window.window_handle();
|
||||
|
||||
if !value.starts_with("nsec") || value.is_empty() {
|
||||
window.push_notification((NotificationType::Warning, "Private Key is required"), cx);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading spinner
|
||||
self.set_loading(true, cx);
|
||||
|
||||
let window_handle = window.window_handle();
|
||||
let keys = if let Ok(keys) = Keys::parse(&value) {
|
||||
keys
|
||||
} else {
|
||||
// TODO: handle error
|
||||
window.push_notification((NotificationType::Warning, "Private Key isn't valid"), cx);
|
||||
return;
|
||||
};
|
||||
|
||||
// Show loading spinner
|
||||
self.set_loading(true, cx);
|
||||
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let client = get_client();
|
||||
let (tx, rx) = oneshot::channel::<NostrProfile>();
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
let public_key = keys.get_public_key().await.unwrap();
|
||||
cx.background_spawn(async move {
|
||||
if let Ok(public_key) = keys.get_public_key().await {
|
||||
let metadata = client
|
||||
.fetch_metadata(public_key, Duration::from_secs(3))
|
||||
.fetch_metadata(public_key, Duration::from_secs(2))
|
||||
.await
|
||||
.ok()
|
||||
.unwrap_or(Metadata::new());
|
||||
let profile = NostrProfile::new(public_key, metadata);
|
||||
.unwrap_or_default();
|
||||
|
||||
_ = tx.send(profile);
|
||||
_ = client.set_signer(keys).await;
|
||||
})
|
||||
.detach();
|
||||
_ = tx.send(NostrProfile::new(public_key, metadata));
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
if let Ok(profile) = rx.await {
|
||||
cx.update_window(window_handle, |_, window, cx| {
|
||||
_ = cx.update_window(window_handle, |_, window, cx| {
|
||||
cx.update_global::<AppRegistry, _>(|this, cx| {
|
||||
this.set_user(Some(profile.clone()));
|
||||
this.set_root_view(app::init(profile, window, cx).into(), cx);
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn render_selection(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("login_connect_btn")
|
||||
.label("Login with Nostr Connect")
|
||||
.primary()
|
||||
.w_full()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.use_connect(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("login_privkey_btn")
|
||||
.label("Login with Private Key")
|
||||
.custom(
|
||||
ButtonCustomVariant::new(window, cx)
|
||||
.color(cx.theme().base.step(cx, ColorScaleStep::THREE))
|
||||
.border(cx.theme().base.step(cx, ColorScaleStep::THREE))
|
||||
.hover(cx.theme().base.step(cx, ColorScaleStep::FOUR))
|
||||
.active(cx.theme().base.step(cx, ColorScaleStep::FIVE))
|
||||
.foreground(cx.theme().base.step(cx, ColorScaleStep::TWELVE)),
|
||||
)
|
||||
.w_full()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.use_privkey(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.my_2()
|
||||
.h_px()
|
||||
.rounded_md()
|
||||
.w_full()
|
||||
.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)),
|
||||
)
|
||||
.child(
|
||||
Button::new("join_btn")
|
||||
.label("Are you new? Join here!")
|
||||
.ghost()
|
||||
.w_full()
|
||||
.on_click(|_, _, cx| {
|
||||
cx.open_url(JOIN_URL);
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_connect_login(&self, cx: &mut Context<Self>) -> Div {
|
||||
let connect_string = self.connect_uri.to_string();
|
||||
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.text_xs()
|
||||
.text_center()
|
||||
.child(
|
||||
div()
|
||||
.font_semibold()
|
||||
.line_height(relative(1.2))
|
||||
.child("Scan this QR Code in the Nostr Signer app"),
|
||||
)
|
||||
.child("Recommend: Amber (Android), nsec.app (web),..."),
|
||||
)
|
||||
.when_some(self.qr_path.clone(), |this, path| {
|
||||
this.child(
|
||||
div()
|
||||
.mb_2()
|
||||
.p_2()
|
||||
.size_72()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_2()
|
||||
.rounded_lg()
|
||||
.shadow_lg()
|
||||
.when(cx.theme().appearance.is_dark(), |this| {
|
||||
this.shadow_none()
|
||||
.border_1()
|
||||
.border_color(cx.theme().base.step(cx, ColorScaleStep::SIX))
|
||||
})
|
||||
.bg(cx.theme().background)
|
||||
.child(img(path).h_64()),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Button::new("copy")
|
||||
.label("Copy Connection String")
|
||||
.primary()
|
||||
.w_full()
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(connect_string.clone()))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("cancel")
|
||||
.label("Cancel")
|
||||
.ghost()
|
||||
.w_full()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.reset(window, cx);
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_privkey_login(&self, cx: &mut Context<Self>) -> Div {
|
||||
div()
|
||||
.w_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.child("Private Key:")
|
||||
.child(self.nsec_input.clone()),
|
||||
)
|
||||
.child(
|
||||
Button::new("login")
|
||||
.label("Login")
|
||||
.primary()
|
||||
.w_full()
|
||||
.loading(self.is_loading)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.privkey_login(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("cancel")
|
||||
.label("Cancel")
|
||||
.ghost()
|
||||
.w_full()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.reset(window, cx);
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for Onboarding {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.size_full()
|
||||
.relative()
|
||||
@@ -149,7 +356,7 @@ impl Render for Onboarding {
|
||||
.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.gap_6()
|
||||
.gap_8()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
@@ -182,62 +389,13 @@ impl Render for Onboarding {
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(div().w_72().map(|this| {
|
||||
.child(div().w_72().map(|_| {
|
||||
if self.use_privkey {
|
||||
this.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.child("Private Key:")
|
||||
.child(self.input.clone()),
|
||||
)
|
||||
.child(
|
||||
Button::new("login")
|
||||
.label("Login")
|
||||
.primary()
|
||||
.w_full()
|
||||
.loading(self.is_loading)
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.login(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("cancel")
|
||||
.label("Cancel")
|
||||
.ghost()
|
||||
.w_full()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.reset(window, cx);
|
||||
})),
|
||||
)
|
||||
self.render_privkey_login(cx)
|
||||
} else if self.use_connect {
|
||||
self.render_connect_login(cx)
|
||||
} else {
|
||||
this.flex()
|
||||
.flex_col()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("login_btn")
|
||||
.label("Login with Private Key")
|
||||
.primary()
|
||||
.w_full()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.use_privkey(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("join_btn")
|
||||
.label("Join Nostr")
|
||||
.ghost()
|
||||
.w_full()
|
||||
.on_click(|_, _, cx| {
|
||||
cx.open_url(JOIN_URL);
|
||||
}),
|
||||
)
|
||||
self.render_selection(window, cx)
|
||||
}
|
||||
})),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user