feat: support nip46
This commit is contained in:
34
Cargo.lock
generated
34
Cargo.lock
generated
@@ -1064,9 +1064,11 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"dirs 5.0.1",
|
||||||
"gpui",
|
"gpui",
|
||||||
"itertools 0.13.0",
|
"itertools 0.13.0",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
|
"qrcode-generator",
|
||||||
"random_name_generator",
|
"random_name_generator",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2324,6 +2326,15 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "html-escape"
|
||||||
|
version = "0.2.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
|
||||||
|
dependencies = [
|
||||||
|
"utf8-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "http"
|
name = "http"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -4034,6 +4045,23 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "qrcode-generator"
|
||||||
|
version = "5.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "faf0051849b5465059b75f59d388c7318aad6554701b74ecf02afc2573b0306c"
|
||||||
|
dependencies = [
|
||||||
|
"html-escape",
|
||||||
|
"image",
|
||||||
|
"qrcodegen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "qrcodegen"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quick-error"
|
name = "quick-error"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@@ -5895,6 +5923,12 @@ version = "1.0.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
|
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8-width"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8_iter"
|
name = "utf8_iter"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
use app_state::registry::AppRegistry;
|
use app_state::registry::AppRegistry;
|
||||||
use common::profile::NostrProfile;
|
use common::{profile::NostrProfile, qr::create_qr};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, prelude::FluentBuilder, relative, svg, App, AppContext, BorrowAppContext, Context, Entity,
|
div, img, prelude::FluentBuilder, relative, svg, App, AppContext, BorrowAppContext,
|
||||||
IntoElement, ParentElement, Render, Styled, Window,
|
ClipboardItem, Context, Div, Entity, IntoElement, ParentElement, Render, Styled, Window,
|
||||||
};
|
};
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use state::get_client;
|
use state::get_client;
|
||||||
use std::time::Duration;
|
use std::{path::PathBuf, time::Duration};
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
use ui::{
|
use ui::{
|
||||||
button::{Button, ButtonVariants},
|
button::{Button, ButtonCustomVariant, ButtonVariants},
|
||||||
input::{InputEvent, TextInput},
|
input::{InputEvent, TextInput},
|
||||||
notification::NotificationType,
|
notification::NotificationType,
|
||||||
theme::{scale::ColorScaleStep, ActiveTheme},
|
theme::{scale::ColorScaleStep, ActiveTheme},
|
||||||
@@ -18,7 +18,7 @@ use ui::{
|
|||||||
|
|
||||||
use super::app;
|
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/";
|
const JOIN_URL: &str = "https://start.njump.me/";
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Onboarding> {
|
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 {
|
pub struct Onboarding {
|
||||||
input: Entity<TextInput>,
|
app_keys: Keys,
|
||||||
|
connect_uri: NostrConnectURI,
|
||||||
|
qr_path: Option<PathBuf>,
|
||||||
|
nsec_input: Entity<TextInput>,
|
||||||
use_connect: bool,
|
use_connect: bool,
|
||||||
use_privkey: bool,
|
use_privkey: bool,
|
||||||
is_loading: bool,
|
is_loading: bool,
|
||||||
@@ -34,26 +37,41 @@ pub struct Onboarding {
|
|||||||
|
|
||||||
impl Onboarding {
|
impl Onboarding {
|
||||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
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)
|
TextInput::new(window, cx)
|
||||||
.text_size(Size::XSmall)
|
.text_size(Size::XSmall)
|
||||||
.placeholder("nsec...")
|
.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| {
|
cx.new(|cx| {
|
||||||
|
// Handle Enter event for nsec input
|
||||||
cx.subscribe_in(
|
cx.subscribe_in(
|
||||||
&input,
|
&nsec_input,
|
||||||
window,
|
window,
|
||||||
move |this: &mut Self, _, input_event, window, cx| {
|
move |this: &mut Self, _, input_event, window, cx| {
|
||||||
if let InputEvent::PressEnter = input_event {
|
if let InputEvent::PressEnter = input_event {
|
||||||
this.login(window, cx);
|
this.privkey_login(window, cx);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
input,
|
app_keys,
|
||||||
|
connect_uri,
|
||||||
|
qr_path,
|
||||||
|
nsec_input,
|
||||||
use_connect: false,
|
use_connect: false,
|
||||||
use_privkey: false,
|
use_privkey: false,
|
||||||
is_loading: 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;
|
self.use_connect = true;
|
||||||
cx.notify();
|
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>) {
|
fn use_privkey(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.use_privkey = true;
|
self.use_privkey = true;
|
||||||
@@ -84,60 +139,212 @@ impl Onboarding {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn login(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn privkey_login(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let value = self.input.read(cx).text().to_string();
|
let value = self.nsec_input.read(cx).text().to_string();
|
||||||
|
let window_handle = window.window_handle();
|
||||||
|
|
||||||
if !value.starts_with("nsec") || value.is_empty() {
|
if !value.starts_with("nsec") || value.is_empty() {
|
||||||
window.push_notification((NotificationType::Warning, "Private Key is required"), cx);
|
window.push_notification((NotificationType::Warning, "Private Key is required"), cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show loading spinner
|
|
||||||
self.set_loading(true, cx);
|
|
||||||
|
|
||||||
let window_handle = window.window_handle();
|
|
||||||
let keys = if let Ok(keys) = Keys::parse(&value) {
|
let keys = if let Ok(keys) = Keys::parse(&value) {
|
||||||
keys
|
keys
|
||||||
} else {
|
} else {
|
||||||
// TODO: handle error
|
window.push_notification((NotificationType::Warning, "Private Key isn't valid"), cx);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Show loading spinner
|
||||||
|
self.set_loading(true, cx);
|
||||||
|
|
||||||
cx.spawn(|_, mut cx| async move {
|
cx.spawn(|_, mut cx| async move {
|
||||||
let client = get_client();
|
let client = get_client();
|
||||||
let (tx, rx) = oneshot::channel::<NostrProfile>();
|
let (tx, rx) = oneshot::channel::<NostrProfile>();
|
||||||
|
|
||||||
cx.background_executor()
|
cx.background_spawn(async move {
|
||||||
.spawn(async move {
|
if let Ok(public_key) = keys.get_public_key().await {
|
||||||
let public_key = keys.get_public_key().await.unwrap();
|
|
||||||
let metadata = client
|
let metadata = client
|
||||||
.fetch_metadata(public_key, Duration::from_secs(3))
|
.fetch_metadata(public_key, Duration::from_secs(2))
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
.unwrap_or(Metadata::new());
|
.unwrap_or_default();
|
||||||
let profile = NostrProfile::new(public_key, metadata);
|
|
||||||
|
|
||||||
_ = tx.send(profile);
|
|
||||||
_ = client.set_signer(keys).await;
|
_ = client.set_signer(keys).await;
|
||||||
})
|
_ = tx.send(NostrProfile::new(public_key, metadata));
|
||||||
.detach();
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
if let Ok(profile) = rx.await {
|
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| {
|
cx.update_global::<AppRegistry, _>(|this, cx| {
|
||||||
this.set_user(Some(profile.clone()));
|
this.set_user(Some(profile.clone()));
|
||||||
this.set_root_view(app::init(profile, window, cx).into(), cx);
|
this.set_root_view(app::init(profile, window, cx).into(), cx);
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.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 {
|
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()
|
div()
|
||||||
.size_full()
|
.size_full()
|
||||||
.relative()
|
.relative()
|
||||||
@@ -149,7 +356,7 @@ impl Render for Onboarding {
|
|||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.items_center()
|
.items_center()
|
||||||
.gap_6()
|
.gap_8()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
@@ -182,62 +389,13 @@ impl Render for Onboarding {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(div().w_72().map(|this| {
|
.child(div().w_72().map(|_| {
|
||||||
if self.use_privkey {
|
if self.use_privkey {
|
||||||
this.flex()
|
self.render_privkey_login(cx)
|
||||||
.flex_col()
|
} else if self.use_connect {
|
||||||
.gap_2()
|
self.render_connect_login(cx)
|
||||||
.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);
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
this.flex()
|
self.render_selection(window, cx)
|
||||||
.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);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use common::{
|
|||||||
constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID},
|
constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID},
|
||||||
profile::NostrProfile,
|
profile::NostrProfile,
|
||||||
};
|
};
|
||||||
use gpui::{AnyView, App, Global, WeakEntity};
|
use gpui::{AnyView, App, AppContext, Global, WeakEntity};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use state::get_client;
|
use state::get_client;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -22,45 +22,44 @@ impl AppRegistry {
|
|||||||
let client = get_client();
|
let client = get_client();
|
||||||
let public_key = profile.public_key();
|
let public_key = profile.public_key();
|
||||||
|
|
||||||
cx.background_executor()
|
cx.background_spawn(async move {
|
||||||
.spawn(async move {
|
let subscription = Filter::new()
|
||||||
let subscription = Filter::new()
|
.kind(Kind::ContactList)
|
||||||
.kind(Kind::ContactList)
|
.author(public_key)
|
||||||
.author(public_key)
|
.limit(1);
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
// Get contact list
|
// Get contact list
|
||||||
_ = client.sync(subscription, &SyncOptions::default()).await;
|
_ = client.sync(subscription, &SyncOptions::default()).await;
|
||||||
|
|
||||||
let all_messages_sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
|
let all_messages_sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID);
|
||||||
let new_message_sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
let new_message_sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID);
|
||||||
|
|
||||||
// Create a filter for getting all gift wrapped events send to current user
|
// Create a filter for getting all gift wrapped events send to current user
|
||||||
let all_messages = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
let all_messages = Filter::new().kind(Kind::GiftWrap).pubkey(public_key);
|
||||||
|
|
||||||
// Create a filter for getting new message
|
// Create a filter for getting new message
|
||||||
let new_message = Filter::new()
|
let new_message = Filter::new()
|
||||||
.kind(Kind::GiftWrap)
|
.kind(Kind::GiftWrap)
|
||||||
.pubkey(public_key)
|
.pubkey(public_key)
|
||||||
.limit(0);
|
.limit(0);
|
||||||
|
|
||||||
// Subscribe for all messages
|
// Subscribe for all messages
|
||||||
_ = client
|
_ = client
|
||||||
.subscribe_with_id(
|
.subscribe_with_id(
|
||||||
all_messages_sub_id,
|
all_messages_sub_id,
|
||||||
all_messages,
|
all_messages,
|
||||||
Some(SubscribeAutoCloseOptions::default().exit_policy(
|
Some(SubscribeAutoCloseOptions::default().exit_policy(
|
||||||
ReqExitPolicy::WaitDurationAfterEOSE(Duration::from_secs(5)),
|
ReqExitPolicy::WaitDurationAfterEOSE(Duration::from_secs(5)),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// Subscribe for new message
|
// Subscribe for new message
|
||||||
_ = client
|
_ = client
|
||||||
.subscribe_with_id(new_message_sub_id, new_message, None)
|
.subscribe_with_id(new_message_sub_id, new_message, None)
|
||||||
.await;
|
.await;
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|||||||
@@ -10,5 +10,7 @@ nostr-sdk.workspace = true
|
|||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
|
dirs.workspace = true
|
||||||
|
|
||||||
random_name_generator = "0.3.6"
|
random_name_generator = "0.3.6"
|
||||||
|
qrcode-generator = "5.0.0"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod constants;
|
pub mod constants;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
|
pub mod qr;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|||||||
13
crates/common/src/qr.rs
Normal file
13
crates/common/src/qr.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use dirs::config_dir;
|
||||||
|
use qrcode_generator::QrCodeEcc;
|
||||||
|
|
||||||
|
pub fn create_qr(data: &str) -> Result<PathBuf, anyhow::Error> {
|
||||||
|
let config_dir = config_dir().expect("Config directory not found");
|
||||||
|
let path = config_dir.join("Coop/nostr_connect.png");
|
||||||
|
|
||||||
|
qrcode_generator::to_png_to_file(data, QrCodeEcc::Low, 1024, &path)?;
|
||||||
|
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
@@ -1504,14 +1504,20 @@ impl EntityInputHandler for TextInput {
|
|||||||
let mut index_offset = 0;
|
let mut index_offset = 0;
|
||||||
|
|
||||||
for line in lines.iter() {
|
for line in lines.iter() {
|
||||||
if let Some(p) = line.position_for_index(range.start - index_offset, line_height) {
|
if let Some(p) =
|
||||||
|
line.position_for_index(range.start.saturating_sub(index_offset), line_height)
|
||||||
|
{
|
||||||
start_origin = Some(p + point(px(0.), y_offset));
|
start_origin = Some(p + point(px(0.), y_offset));
|
||||||
}
|
}
|
||||||
if let Some(p) = line.position_for_index(range.end - index_offset, line_height) {
|
|
||||||
|
if let Some(p) =
|
||||||
|
line.position_for_index(range.end.saturating_sub(index_offset), line_height)
|
||||||
|
{
|
||||||
end_origin = Some(p + point(px(0.), y_offset));
|
end_origin = Some(p + point(px(0.), y_offset));
|
||||||
}
|
}
|
||||||
|
|
||||||
y_offset += line.size(line_height).height;
|
y_offset += line.size(line_height).height;
|
||||||
|
|
||||||
if start_origin.is_some() && end_origin.is_some() {
|
if start_origin.is_some() && end_origin.is_some() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1519,18 +1525,30 @@ impl EntityInputHandler for TextInput {
|
|||||||
index_offset += line.len();
|
index_offset += line.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let start_origin = start_origin.unwrap_or_default();
|
||||||
|
let end_origin = end_origin.unwrap_or_default();
|
||||||
|
|
||||||
Some(Bounds::from_corners(
|
Some(Bounds::from_corners(
|
||||||
bounds.origin + start_origin.unwrap_or_default(),
|
bounds.origin + start_origin,
|
||||||
bounds.origin + end_origin.unwrap_or_default(),
|
// + line_height for show IME panel under the cursor line.
|
||||||
|
bounds.origin + point(end_origin.x, end_origin.y + line_height),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn character_index_for_point(
|
fn character_index_for_point(
|
||||||
&mut self,
|
&mut self,
|
||||||
_point: gpui::Point<Pixels>,
|
point: gpui::Point<Pixels>,
|
||||||
_window: &mut Window,
|
_window: &mut Window,
|
||||||
_cx: &mut Context<Self>,
|
_cx: &mut Context<Self>,
|
||||||
) -> Option<usize> {
|
) -> Option<usize> {
|
||||||
|
let line_height = self.last_line_height;
|
||||||
|
let line_point = self.last_bounds?.localize(&point)?;
|
||||||
|
let lines = self.last_layout.as_ref()?;
|
||||||
|
for line in lines.iter() {
|
||||||
|
if let Ok(utf8_index) = line.index_for_position(line_point, line_height) {
|
||||||
|
return Some(self.offset_to_utf16(utf8_index));
|
||||||
|
}
|
||||||
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user