.
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m26s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m22s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 1m26s
Rust / build (ubuntu-latest, stable) (pull_request) Failing after 1m22s
Rust / build (macos-latest, stable) (push) Has been cancelled
Rust / build (windows-latest, stable) (push) Has been cancelled
Rust / build (macos-latest, stable) (pull_request) Has been cancelled
Rust / build (windows-latest, stable) (pull_request) Has been cancelled
This commit is contained in:
3
assets/icons/device.svg
Normal file
3
assets/icons/device.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M14.25 10.75C14.25 9.64543 15.1454 8.75 16.25 8.75H20.25C21.3546 8.75 22.25 9.64543 22.25 10.75V19.25C22.25 20.3546 21.3546 21.25 20.25 21.25H16.25C15.1454 21.25 14.25 20.3546 14.25 19.25V10.75Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M17.25 18.25H19.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M20.25 8.75V5.75C20.25 4.64543 19.3546 3.75 18.25 3.75H5.75C4.64543 3.75 3.75 4.64543 3.75 5.75V14.75C3.75 15.8546 2.85457 16.75 1.75 16.75V18.25C1.75 19.3546 2.64543 20.25 3.75 20.25H14.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M3.75 16.75H14.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 898 B |
3
assets/icons/scan.svg
Normal file
3
assets/icons/scan.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path d="M7.25 4.75H4.75C3.64543 4.75 2.75 5.64543 2.75 6.75V9.25M16.75 4.75H19.25C20.3546 4.75 21.25 5.64543 21.25 6.75V9.25M21.25 14.75V17.25C21.25 18.3546 20.3546 19.25 19.25 19.25H16.75M7.25 19.25H4.75C3.64543 19.25 2.75 18.3546 2.75 17.25V14.75M7.75 9.75V14.25M16.25 9.75V14.25M12 9.75V12.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 468 B |
@@ -113,7 +113,7 @@ impl ChatRegistry {
|
|||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Observe the nip65 state and load chat rooms on every state change
|
// Observe the nip65 state and load chat rooms on every state change
|
||||||
cx.observe(&nostr, |this, state, cx| {
|
cx.observe(&nostr, |this, state, cx| {
|
||||||
match state.read(cx).relay_list_state() {
|
match state.read(cx).relay_list_state {
|
||||||
RelayState::Idle => {
|
RelayState::Idle => {
|
||||||
this.reset(cx);
|
this.reset(cx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -876,7 +876,7 @@ impl ChatPanel {
|
|||||||
window.open_modal(cx, move |this, _window, cx| {
|
window.open_modal(cx, move |this, _window, cx| {
|
||||||
this.show_close(true)
|
this.show_close(true)
|
||||||
.title(SharedString::from("Sent Reports"))
|
.title(SharedString::from("Sent Reports"))
|
||||||
.child(v_flex().pb_4().gap_4().children({
|
.child(v_flex().pb_2().gap_4().children({
|
||||||
let mut items = Vec::with_capacity(reports.len());
|
let mut items = Vec::with_capacity(reports.len());
|
||||||
|
|
||||||
for report in reports.iter() {
|
for report in reports.iter() {
|
||||||
|
|||||||
187
crates/coop/src/dialogs/accounts.rs
Normal file
187
crates/coop/src/dialogs/accounts.rs
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
use anyhow::Error;
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
div, px, App, AppContext, Context, Entity, InteractiveElement, IntoElement, ParentElement,
|
||||||
|
Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window,
|
||||||
|
};
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use person::PersonRegistry;
|
||||||
|
use state::{NostrRegistry, SignerEvent};
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::avatar::Avatar;
|
||||||
|
use ui::button::{Button, ButtonVariants};
|
||||||
|
use ui::{h_flex, v_flex, Icon, IconName, Sizable, WindowExtension};
|
||||||
|
|
||||||
|
use crate::dialogs::connect::ConnectSigner;
|
||||||
|
use crate::dialogs::import::ImportKey;
|
||||||
|
|
||||||
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<AccountSelector> {
|
||||||
|
cx.new(|cx| AccountSelector::new(window, cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Account selector
|
||||||
|
pub struct AccountSelector {
|
||||||
|
/// The error message displayed when an error occurs.
|
||||||
|
error: Entity<Option<SharedString>>,
|
||||||
|
|
||||||
|
/// Async tasks
|
||||||
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
|
|
||||||
|
/// Subscription to the signer events
|
||||||
|
_subscription: Option<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccountSelector {
|
||||||
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let error = cx.new(|_| None);
|
||||||
|
|
||||||
|
// Subscribe to the signer events
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, window, cx| {
|
||||||
|
match event {
|
||||||
|
SignerEvent::Set => {
|
||||||
|
window.close_all_modals(cx);
|
||||||
|
window.refresh();
|
||||||
|
}
|
||||||
|
SignerEvent::Error(e) => {
|
||||||
|
this.error.update(cx, |this, cx| {
|
||||||
|
*this = Some(e.into());
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
error,
|
||||||
|
tasks: vec![],
|
||||||
|
_subscription: Some(subscription),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn login(&mut self, public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let task = nostr.read(cx).get_signer(&public_key, cx);
|
||||||
|
let error = self.error.downgrade();
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |_this, cx| {
|
||||||
|
match task.await {
|
||||||
|
Ok(signer) => {
|
||||||
|
nostr.update(cx, |this, cx| {
|
||||||
|
this.set_signer(signer, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error.update(cx, |this, cx| {
|
||||||
|
*this = Some(e.to_string().into());
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_import(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let import = cx.new(|cx| ImportKey::new(window, cx));
|
||||||
|
|
||||||
|
window.open_modal(cx, move |this, _window, _cx| {
|
||||||
|
this.width(px(460.))
|
||||||
|
.title("Import a Secret Key or Bunker Connection")
|
||||||
|
.show_close(true)
|
||||||
|
.pb_2()
|
||||||
|
.child(import.clone())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_connect(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let connect = cx.new(|cx| ConnectSigner::new(window, cx));
|
||||||
|
|
||||||
|
window.open_modal(cx, move |this, _window, _cx| {
|
||||||
|
this.width(px(460.))
|
||||||
|
.title("Scan QR Code to Connect")
|
||||||
|
.show_close(true)
|
||||||
|
.pb_2()
|
||||||
|
.child(connect.clone())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for AccountSelector {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let persons = PersonRegistry::global(cx);
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let npubs = nostr.read(cx).npubs();
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.size_full()
|
||||||
|
.gap_2()
|
||||||
|
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.italic()
|
||||||
|
.text_xs()
|
||||||
|
.text_center()
|
||||||
|
.text_color(cx.theme().danger_active)
|
||||||
|
.child(error.clone()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.children({
|
||||||
|
let mut items = vec![];
|
||||||
|
|
||||||
|
for (ix, public_key) in npubs.read(cx).iter().enumerate() {
|
||||||
|
let profile = persons.read(cx).get(public_key, cx);
|
||||||
|
|
||||||
|
items.push(
|
||||||
|
h_flex()
|
||||||
|
.id(ix)
|
||||||
|
.group("")
|
||||||
|
.px_2()
|
||||||
|
.h_10()
|
||||||
|
.gap_2()
|
||||||
|
.w_full()
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.bg(cx.theme().ghost_element_background)
|
||||||
|
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
||||||
|
.child(Avatar::new(profile.avatar()).small())
|
||||||
|
.child(div().text_sm().child(profile.name()))
|
||||||
|
.on_click(cx.listener({
|
||||||
|
let public_key = *public_key;
|
||||||
|
move |this, _ev, window, cx| {
|
||||||
|
this.login(public_key, window, cx);
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
items
|
||||||
|
})
|
||||||
|
.child(div().w_full().h_px().bg(cx.theme().border_variant))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.justify_end()
|
||||||
|
.w_full()
|
||||||
|
.child(
|
||||||
|
Button::new("input")
|
||||||
|
.icon(Icon::new(IconName::Usb))
|
||||||
|
.label("Import")
|
||||||
|
.ghost()
|
||||||
|
.small()
|
||||||
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
|
this.open_import(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("qr")
|
||||||
|
.icon(Icon::new(IconName::Scan))
|
||||||
|
.label("Scan QR to connect")
|
||||||
|
.ghost()
|
||||||
|
.small()
|
||||||
|
.on_click(cx.listener(move |this, _ev, window, cx| {
|
||||||
|
this.open_connect(window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
115
crates/coop/src/dialogs/connect.rs
Normal file
115
crates/coop/src/dialogs/connect.rs
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use common::TextUtils;
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
div, img, px, AppContext, Context, Entity, Image, IntoElement, ParentElement, Render,
|
||||||
|
SharedString, Styled, Subscription, Window,
|
||||||
|
};
|
||||||
|
use nostr_connect::prelude::*;
|
||||||
|
use state::{
|
||||||
|
CoopAuthUrlHandler, NostrRegistry, SignerEvent, CLIENT_NAME, NOSTR_CONNECT_RELAY,
|
||||||
|
NOSTR_CONNECT_TIMEOUT,
|
||||||
|
};
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::v_flex;
|
||||||
|
|
||||||
|
pub struct ConnectSigner {
|
||||||
|
/// QR Code
|
||||||
|
qr_code: Option<Arc<Image>>,
|
||||||
|
|
||||||
|
/// Error message
|
||||||
|
error: Entity<Option<SharedString>>,
|
||||||
|
|
||||||
|
/// Subscription to the signer event
|
||||||
|
_subscription: Option<Subscription>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConnectSigner {
|
||||||
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let error = cx.new(|_| None);
|
||||||
|
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let app_keys = nostr.read(cx).app_keys.clone();
|
||||||
|
|
||||||
|
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||||
|
let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap();
|
||||||
|
|
||||||
|
// Generate the nostr connect uri
|
||||||
|
let uri = NostrConnectUri::client(app_keys.public_key(), vec![relay], CLIENT_NAME);
|
||||||
|
|
||||||
|
// Generate the nostr connect
|
||||||
|
let mut signer = NostrConnect::new(uri.clone(), app_keys.clone(), timeout, None).unwrap();
|
||||||
|
|
||||||
|
// Handle the auth request
|
||||||
|
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||||
|
|
||||||
|
// Generate a QR code for quick connection
|
||||||
|
let qr_code = uri.to_string().to_qr();
|
||||||
|
|
||||||
|
// Set signer in the background
|
||||||
|
nostr.update(cx, |this, cx| {
|
||||||
|
this.add_nip46_signer(&signer, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscribe to the signer event
|
||||||
|
let subscription = cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| {
|
||||||
|
if let SignerEvent::Error(e) = event {
|
||||||
|
this.set_error(e, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
qr_code,
|
||||||
|
error,
|
||||||
|
_subscription: Some(subscription),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error<S>(&mut self, message: S, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
S: Into<SharedString>,
|
||||||
|
{
|
||||||
|
self.error.update(cx, |this, cx| {
|
||||||
|
*this = Some(message.into());
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ConnectSigner {
|
||||||
|
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
const MSG: &str = "Scan with any Nostr Connect-compatible app to connect";
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.size_full()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.p_4()
|
||||||
|
.when_some(self.qr_code.as_ref(), |this, qr| {
|
||||||
|
this.child(
|
||||||
|
img(qr.clone())
|
||||||
|
.size(px(256.))
|
||||||
|
.rounded(cx.theme().radius_lg)
|
||||||
|
.border_1()
|
||||||
|
.border_color(cx.theme().border),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_center()
|
||||||
|
.text_color(cx.theme().danger_active)
|
||||||
|
.child(error.clone()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(SharedString::from(MSG)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
301
crates/coop/src/dialogs/import.rs
Normal file
301
crates/coop/src/dialogs/import.rs
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Error};
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
|
use gpui::{
|
||||||
|
div, AppContext, Context, Entity, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||||
|
Subscription, Task, Window,
|
||||||
|
};
|
||||||
|
use nostr_connect::prelude::*;
|
||||||
|
use smallvec::{smallvec, SmallVec};
|
||||||
|
use state::{CoopAuthUrlHandler, NostrRegistry, SignerEvent};
|
||||||
|
use theme::ActiveTheme;
|
||||||
|
use ui::button::{Button, ButtonVariants};
|
||||||
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
|
use ui::{v_flex, Disableable};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ImportKey {
|
||||||
|
/// Secret key input
|
||||||
|
key_input: Entity<InputState>,
|
||||||
|
|
||||||
|
/// Password input (if required)
|
||||||
|
pass_input: Entity<InputState>,
|
||||||
|
|
||||||
|
/// Error message
|
||||||
|
error: Entity<Option<SharedString>>,
|
||||||
|
|
||||||
|
/// Countdown timer for nostr connect
|
||||||
|
countdown: Entity<Option<u64>>,
|
||||||
|
|
||||||
|
/// Whether the user is currently loading
|
||||||
|
loading: bool,
|
||||||
|
|
||||||
|
/// Async tasks
|
||||||
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
|
|
||||||
|
/// Event subscriptions
|
||||||
|
_subscriptions: SmallVec<[Subscription; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImportKey {
|
||||||
|
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let key_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
||||||
|
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![];
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Subscribe to key input events and process login when the user presses enter
|
||||||
|
cx.subscribe_in(&key_input, window, |this, _input, event, window, cx| {
|
||||||
|
if let InputEvent::PressEnter { .. } = event {
|
||||||
|
this.login(window, cx);
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Subscribe to the nostr signer event
|
||||||
|
cx.subscribe_in(&nostr, window, |this, _state, event, _window, cx| {
|
||||||
|
if let SignerEvent::Error(e) = event {
|
||||||
|
this.set_error(e, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
key_input,
|
||||||
|
pass_input,
|
||||||
|
error,
|
||||||
|
countdown,
|
||||||
|
loading: false,
|
||||||
|
tasks: vec![],
|
||||||
|
_subscriptions: subscriptions,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn login(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if self.loading {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
// Prevent duplicate login requests
|
||||||
|
self.set_loading(true, cx);
|
||||||
|
|
||||||
|
let value = self.key_input.read(cx).value();
|
||||||
|
let password = self.pass_input.read(cx).value();
|
||||||
|
|
||||||
|
if value.starts_with("bunker://") {
|
||||||
|
self.bunker(&value, window, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if value.starts_with("ncryptsec1") {
|
||||||
|
self.ncryptsec(value, password, window, cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(secret) = SecretKey::parse(&value) {
|
||||||
|
let keys = Keys::new(secret);
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
|
||||||
|
// Update the signer
|
||||||
|
nostr.update(cx, |this, cx| {
|
||||||
|
this.set_signer(keys, cx);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.set_error("Invalid key", cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bunker(&mut self, content: &str, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let Ok(uri) = NostrConnectUri::parse(content) else {
|
||||||
|
self.set_error("Bunker is not valid", cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let app_keys = nostr.read(cx).app_keys.clone();
|
||||||
|
let timeout = Duration::from_secs(30);
|
||||||
|
|
||||||
|
// Construct the nostr connect signer
|
||||||
|
let mut signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap();
|
||||||
|
|
||||||
|
// Handle auth url with the default browser
|
||||||
|
signer.auth_url_handler(CoopAuthUrlHandler);
|
||||||
|
|
||||||
|
// Set signer in the background
|
||||||
|
nostr.update(cx, |this, cx| {
|
||||||
|
this.add_nip46_signer(&signer, cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start countdown
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
for i in (0..=30).rev() {
|
||||||
|
if i == 0 {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_countdown(None, cx);
|
||||||
|
})?;
|
||||||
|
} else {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_countdown(Some(i), cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
cx.background_executor().timer(Duration::from_secs(1)).await;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ncryptsec<S>(&mut self, content: S, pwd: S, window: &mut Window, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
S: Into<String>,
|
||||||
|
{
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let content: String = content.into();
|
||||||
|
let password: String = pwd.into();
|
||||||
|
|
||||||
|
if password.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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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"))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||||
|
match task.await {
|
||||||
|
Ok(keys) => {
|
||||||
|
nostr.update(cx, |this, cx| {
|
||||||
|
this.add_key_signer(&keys, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.set_error(e.to_string(), cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error<S>(&mut self, message: S, cx: &mut Context<Self>)
|
||||||
|
where
|
||||||
|
S: Into<SharedString>,
|
||||||
|
{
|
||||||
|
// Reset the log in state
|
||||||
|
self.set_loading(false, cx);
|
||||||
|
|
||||||
|
// Reset the countdown
|
||||||
|
self.set_countdown(None, cx);
|
||||||
|
|
||||||
|
// Update error message
|
||||||
|
self.error.update(cx, |this, cx| {
|
||||||
|
*this = Some(message.into());
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear the error message after 3 secs
|
||||||
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
|
cx.background_executor().timer(Duration::from_secs(3)).await;
|
||||||
|
|
||||||
|
this.update(cx, |this, cx| {
|
||||||
|
this.error.update(cx, |this, cx| {
|
||||||
|
*this = None;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_loading(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||||
|
self.loading = status;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_countdown(&mut self, i: Option<u64>, cx: &mut Context<Self>) {
|
||||||
|
self.countdown.update(cx, |this, cx| {
|
||||||
|
*this = i;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ImportKey {
|
||||||
|
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.size_full()
|
||||||
|
.p_4()
|
||||||
|
.gap_2()
|
||||||
|
.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.key_input.read(cx).value().starts_with("ncryptsec1"),
|
||||||
|
|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("Continue")
|
||||||
|
.primary()
|
||||||
|
.loading(self.loading)
|
||||||
|
.disabled(self.loading)
|
||||||
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
|
this.login(window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.when_some(self.countdown.read(cx).as_ref(), |this, i| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_center()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.child(SharedString::from(format!(
|
||||||
|
"Approve connection request from your signer in {} seconds",
|
||||||
|
i
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when_some(self.error.read(cx).as_ref(), |this, error| {
|
||||||
|
this.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_center()
|
||||||
|
.text_color(cx.theme().danger_active)
|
||||||
|
.child(error.clone()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,6 @@
|
|||||||
|
pub mod accounts;
|
||||||
pub mod screening;
|
pub mod screening;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
|
|
||||||
|
mod connect;
|
||||||
|
mod import;
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ impl Screening {
|
|||||||
let total = contacts.len();
|
let total = contacts.len();
|
||||||
|
|
||||||
this.title(SharedString::from("Mutual contacts")).child(
|
this.title(SharedString::from("Mutual contacts")).child(
|
||||||
v_flex().gap_1().pb_4().child(
|
v_flex().gap_1().pb_2().child(
|
||||||
uniform_list("contacts", total, move |range, _window, cx| {
|
uniform_list("contacts", total, move |range, _window, cx| {
|
||||||
let persons = PersonRegistry::global(cx);
|
let persons = PersonRegistry::global(cx);
|
||||||
let mut items = Vec::with_capacity(total);
|
let mut items = Vec::with_capacity(total);
|
||||||
@@ -356,9 +356,9 @@ impl Render for Screening {
|
|||||||
.child(
|
.child(
|
||||||
Button::new("report")
|
Button::new("report")
|
||||||
.tooltip("Report as a scam or impostor")
|
.tooltip("Report as a scam or impostor")
|
||||||
.icon(IconName::Boom)
|
.icon(IconName::Warning)
|
||||||
.small()
|
.small()
|
||||||
.danger()
|
.warning()
|
||||||
.rounded()
|
.rounded()
|
||||||
.on_click(cx.listener(move |this, _e, window, cx| {
|
.on_click(cx.listener(move |this, _e, window, cx| {
|
||||||
this.report(window, cx);
|
this.report(window, cx);
|
||||||
|
|||||||
@@ -1,127 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use common::TextUtils;
|
|
||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
div, img, px, relative, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
|
||||||
FocusHandle, Focusable, Image, IntoElement, ParentElement, Render, SharedString, Styled, Task,
|
|
||||||
Window,
|
|
||||||
};
|
|
||||||
use smallvec::{smallvec, SmallVec};
|
|
||||||
use state::NostrRegistry;
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
|
||||||
use ui::dock_area::ClosePanel;
|
|
||||||
use ui::notification::Notification;
|
|
||||||
use ui::{v_flex, StyledExt, WindowExtension};
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ConnectPanel> {
|
|
||||||
cx.new(|cx| ConnectPanel::new(window, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ConnectPanel {
|
|
||||||
name: SharedString,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
|
|
||||||
/// QR Code
|
|
||||||
qr_code: Option<Arc<Image>>,
|
|
||||||
|
|
||||||
/// Background tasks
|
|
||||||
_tasks: SmallVec<[Task<()>; 1]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConnectPanel {
|
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let weak_state = nostr.downgrade();
|
|
||||||
let (signer, uri) = nostr.read(cx).client_connect(None);
|
|
||||||
|
|
||||||
// Generate a QR code for quick connection
|
|
||||||
let qr_code = uri.to_string().to_qr();
|
|
||||||
|
|
||||||
let mut tasks = smallvec![];
|
|
||||||
|
|
||||||
tasks.push(
|
|
||||||
// Wait for nostr connect
|
|
||||||
cx.spawn_in(window, async move |_this, cx| {
|
|
||||||
let result = signer.bunker_uri().await;
|
|
||||||
|
|
||||||
weak_state
|
|
||||||
.update_in(cx, |this, window, cx| {
|
|
||||||
match result {
|
|
||||||
Ok(uri) => {
|
|
||||||
this.persist_bunker(uri, cx);
|
|
||||||
this.set_signer(signer, true, cx);
|
|
||||||
// Close the current panel after setting the signer
|
|
||||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
window.push_notification(Notification::error(e.to_string()), cx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
name: "Nostr Connect".into(),
|
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
qr_code,
|
|
||||||
_tasks: tasks,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Panel for ConnectPanel {
|
|
||||||
fn panel_id(&self) -> SharedString {
|
|
||||||
self.name.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn title(&self, _cx: &App) -> AnyElement {
|
|
||||||
self.name.clone().into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for ConnectPanel {}
|
|
||||||
|
|
||||||
impl Focusable for ConnectPanel {
|
|
||||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ConnectPanel {
|
|
||||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
v_flex()
|
|
||||||
.size_full()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.p_2()
|
|
||||||
.gap_10()
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.justify_center()
|
|
||||||
.items_center()
|
|
||||||
.text_center()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.font_semibold()
|
|
||||||
.line_height(relative(1.25))
|
|
||||||
.child(SharedString::from("Continue with Nostr Connect")),
|
|
||||||
)
|
|
||||||
.child(div().text_sm().text_color(cx.theme().text_muted).child(
|
|
||||||
SharedString::from("Use Nostr Connect apps to scan the code"),
|
|
||||||
)),
|
|
||||||
)
|
|
||||||
.when_some(self.qr_code.as_ref(), |this, qr| {
|
|
||||||
this.child(
|
|
||||||
img(qr.clone())
|
|
||||||
.size(px(256.))
|
|
||||||
.rounded(cx.theme().radius_lg)
|
|
||||||
.border_1()
|
|
||||||
.border_color(cx.theme().border),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -86,7 +86,7 @@ impl Render for GreeterPanel {
|
|||||||
let nip17 = chat.read(cx).state(cx);
|
let nip17 = chat.read(cx).state(cx);
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let nip65 = nostr.read(cx).relay_list_state();
|
let nip65 = nostr.read(cx).relay_list_state.clone();
|
||||||
|
|
||||||
let required_actions =
|
let required_actions =
|
||||||
nip65 == RelayState::NotConfigured || nip17 == InboxState::RelayNotAvailable;
|
nip65 == RelayState::NotConfigured || nip17 == InboxState::RelayNotAvailable;
|
||||||
@@ -188,48 +188,6 @@ impl Render for GreeterPanel {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_2()
|
|
||||||
.w_full()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.w_full()
|
|
||||||
.text_xs()
|
|
||||||
.font_semibold()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from("Use your own identity"))
|
|
||||||
.child(div().flex_1().h_px().bg(cx.theme().border)),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_2()
|
|
||||||
.w_full()
|
|
||||||
.child(
|
|
||||||
Button::new("connect")
|
|
||||||
.icon(Icon::new(IconName::Door))
|
|
||||||
.label("Connect account via Nostr Connect")
|
|
||||||
.ghost()
|
|
||||||
.small()
|
|
||||||
.justify_start()
|
|
||||||
.on_click(move |_ev, window, cx| {
|
|
||||||
//
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("import")
|
|
||||||
.icon(Icon::new(IconName::Usb))
|
|
||||||
.label("Import a secret key or bunker")
|
|
||||||
.ghost()
|
|
||||||
.small()
|
|
||||||
.justify_start()
|
|
||||||
.on_click(move |_ev, window, cx| {
|
|
||||||
//
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
|
|||||||
@@ -1,371 +0,0 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
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, Window,
|
|
||||||
};
|
|
||||||
use nostr_connect::prelude::*;
|
|
||||||
use smallvec::{smallvec, SmallVec};
|
|
||||||
use state::{CoopAuthUrlHandler, NostrRegistry};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::button::{Button, ButtonVariants};
|
|
||||||
use ui::dock_area::panel::{Panel, PanelEvent};
|
|
||||||
use ui::dock_area::ClosePanel;
|
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
|
||||||
use ui::notification::Notification;
|
|
||||||
use ui::{v_flex, Disableable, StyledExt, WindowExtension};
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ImportPanel> {
|
|
||||||
cx.new(|cx| ImportPanel::new(window, cx))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ImportPanel {
|
|
||||||
name: SharedString,
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
|
|
||||||
/// Secret key input
|
|
||||||
key_input: Entity<InputState>,
|
|
||||||
|
|
||||||
/// Password input (if required)
|
|
||||||
pass_input: Entity<InputState>,
|
|
||||||
|
|
||||||
/// Error message
|
|
||||||
error: Entity<Option<SharedString>>,
|
|
||||||
|
|
||||||
/// Countdown timer for nostr connect
|
|
||||||
countdown: Entity<Option<u64>>,
|
|
||||||
|
|
||||||
/// Whether the user is currently logging in
|
|
||||||
logging_in: bool,
|
|
||||||
|
|
||||||
/// Event subscriptions
|
|
||||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ImportPanel {
|
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
|
||||||
let key_input = cx.new(|cx| InputState::new(window, cx).masked(true));
|
|
||||||
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![];
|
|
||||||
|
|
||||||
subscriptions.push(
|
|
||||||
// Subscribe to key input events and process login when the user presses enter
|
|
||||||
cx.subscribe_in(&key_input, window, |this, _input, event, window, cx| {
|
|
||||||
if let InputEvent::PressEnter { .. } = event {
|
|
||||||
this.login(window, cx);
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
key_input,
|
|
||||||
pass_input,
|
|
||||||
error,
|
|
||||||
countdown,
|
|
||||||
name: "Import".into(),
|
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
logging_in: false,
|
|
||||||
_subscriptions: subscriptions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn login(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if self.logging_in {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
// Prevent duplicate login requests
|
|
||||||
self.set_logging_in(true, cx);
|
|
||||||
|
|
||||||
let value = self.key_input.read(cx).value();
|
|
||||||
let password = self.pass_input.read(cx).value();
|
|
||||||
|
|
||||||
if value.starts_with("bunker://") {
|
|
||||||
self.login_with_bunker(&value, window, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if value.starts_with("ncryptsec1") {
|
|
||||||
self.login_with_password(&value, &password, window, cx);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(secret) = SecretKey::parse(&value) {
|
|
||||||
let keys = Keys::new(secret);
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
// Update the signer
|
|
||||||
nostr.update(cx, |this, cx| {
|
|
||||||
this.set_signer(keys, true, cx);
|
|
||||||
});
|
|
||||||
// Close the current panel after setting the signer
|
|
||||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
|
||||||
} else {
|
|
||||||
self.set_error("Invalid", cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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("Bunker is not valid", cx);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
let weak_state = nostr.downgrade();
|
|
||||||
|
|
||||||
let app_keys = nostr.read(cx).app_keys();
|
|
||||||
let timeout = Duration::from_secs(30);
|
|
||||||
let mut signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap();
|
|
||||||
|
|
||||||
// Handle auth url with the default browser
|
|
||||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
|
||||||
|
|
||||||
// Start countdown
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
for i in (0..=30).rev() {
|
|
||||||
if i == 0 {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_countdown(None, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
} else {
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_countdown(Some(i), cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
cx.background_executor().timer(Duration::from_secs(1)).await;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
// Handle connection
|
|
||||||
cx.spawn_in(window, async move |_this, cx| {
|
|
||||||
let result = signer.bunker_uri().await;
|
|
||||||
|
|
||||||
weak_state
|
|
||||||
.update_in(cx, |this, window, cx| {
|
|
||||||
match result {
|
|
||||||
Ok(uri) => {
|
|
||||||
this.persist_bunker(uri, cx);
|
|
||||||
this.set_signer(signer, true, cx);
|
|
||||||
// Close the current panel after setting the signer
|
|
||||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
window.push_notification(Notification::error(e.to_string()), cx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn login_with_password(
|
|
||||||
&mut self,
|
|
||||||
content: &str,
|
|
||||||
pwd: &str,
|
|
||||||
window: &mut Window,
|
|
||||||
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_in(window, async move |this, cx| {
|
|
||||||
let result = task.await;
|
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
match result {
|
|
||||||
Ok(keys) => {
|
|
||||||
let nostr = NostrRegistry::global(cx);
|
|
||||||
// Update the signer
|
|
||||||
nostr.update(cx, |this, cx| {
|
|
||||||
this.set_signer(keys, true, cx);
|
|
||||||
});
|
|
||||||
// Close the current panel after setting the signer
|
|
||||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
this.set_error(e.to_string(), cx);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.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);
|
|
||||||
|
|
||||||
// Reset the countdown
|
|
||||||
self.set_countdown(None, cx);
|
|
||||||
|
|
||||||
// Update error message
|
|
||||||
self.error.update(cx, |this, cx| {
|
|
||||||
*this = Some(message.into());
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear the error message after 3 secs
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
cx.background_executor().timer(Duration::from_secs(3)).await;
|
|
||||||
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.error.update(cx, |this, cx| {
|
|
||||||
*this = None;
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_logging_in(&mut self, status: bool, cx: &mut Context<Self>) {
|
|
||||||
self.logging_in = status;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_countdown(&mut self, i: Option<u64>, cx: &mut Context<Self>) {
|
|
||||||
self.countdown.update(cx, |this, cx| {
|
|
||||||
*this = i;
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Panel for ImportPanel {
|
|
||||||
fn panel_id(&self) -> SharedString {
|
|
||||||
self.name.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn title(&self, _cx: &App) -> AnyElement {
|
|
||||||
self.name.clone().into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for ImportPanel {}
|
|
||||||
|
|
||||||
impl Focusable for ImportPanel {
|
|
||||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ImportPanel {
|
|
||||||
fn render(&mut self, _window: &mut gpui::Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
const SECRET_WARN: &str = "* Coop doesn't store your secret key. \
|
|
||||||
It will be cleared when you close the app. \
|
|
||||||
To persist your identity, please connect via Nostr Connect.";
|
|
||||||
|
|
||||||
v_flex()
|
|
||||||
.size_full()
|
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.p_2()
|
|
||||||
.gap_10()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.text_center()
|
|
||||||
.font_semibold()
|
|
||||||
.line_height(relative(1.25))
|
|
||||||
.child(SharedString::from("Import a Secret Key or Bunker")),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.w_112()
|
|
||||||
.gap_2()
|
|
||||||
.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.key_input.read(cx).value().starts_with("ncryptsec1"),
|
|
||||||
|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("Continue")
|
|
||||||
.primary()
|
|
||||||
.loading(self.logging_in)
|
|
||||||
.disabled(self.logging_in)
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
|
||||||
this.login(window, cx);
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.when_some(self.countdown.read(cx).as_ref(), |this, i| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.text_xs()
|
|
||||||
.text_center()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from(format!(
|
|
||||||
"Approve connection request from your signer in {} seconds",
|
|
||||||
i
|
|
||||||
))),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.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.clone()),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.mt_2()
|
|
||||||
.italic()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(SharedString::from(SECRET_WARN)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -23,7 +23,7 @@ use ui::menu::{DropdownMenu, PopupMenuItem};
|
|||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::{h_flex, v_flex, IconName, Root, Sizable, WindowExtension};
|
use ui::{h_flex, v_flex, IconName, Root, Sizable, WindowExtension};
|
||||||
|
|
||||||
use crate::dialogs::settings;
|
use crate::dialogs::{accounts, settings};
|
||||||
use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list};
|
use crate::panels::{backup, contact_list, greeter, messaging_relays, profile, relay_list};
|
||||||
use crate::sidebar;
|
use crate::sidebar;
|
||||||
|
|
||||||
@@ -64,11 +64,13 @@ pub struct Workspace {
|
|||||||
dock: Entity<DockArea>,
|
dock: Entity<DockArea>,
|
||||||
|
|
||||||
/// Event subscriptions
|
/// Event subscriptions
|
||||||
_subscriptions: SmallVec<[Subscription; 3]>,
|
_subscriptions: SmallVec<[Subscription; 4]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Workspace {
|
impl Workspace {
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let npubs = nostr.read(cx).npubs();
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let titlebar = cx.new(|_| TitleBar::new());
|
let titlebar = cx.new(|_| TitleBar::new());
|
||||||
let dock = cx.new(|cx| DockArea::new(window, cx));
|
let dock = cx.new(|cx| DockArea::new(window, cx));
|
||||||
@@ -82,6 +84,24 @@ impl Workspace {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Observe the nostr entity
|
||||||
|
cx.observe_in(&nostr, window, move |this, nostr, window, cx| {
|
||||||
|
if nostr.read(cx).connected {
|
||||||
|
this.set_layout(window, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Observe the npubs entity
|
||||||
|
cx.observe_in(&npubs, window, move |this, npubs, window, cx| {
|
||||||
|
if !npubs.read(cx).is_empty() {
|
||||||
|
this.account_selector(window, cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Observe all events emitted by the chat registry
|
// Observe all events emitted by the chat registry
|
||||||
cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| {
|
cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| {
|
||||||
@@ -126,11 +146,6 @@ impl Workspace {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set the default layout for app's dock
|
|
||||||
cx.defer_in(window, |this, window, cx| {
|
|
||||||
this.set_layout(window, cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
titlebar,
|
titlebar,
|
||||||
dock,
|
dock,
|
||||||
@@ -206,7 +221,7 @@ impl Workspace {
|
|||||||
window.open_modal(cx, move |this, _window, _cx| {
|
window.open_modal(cx, move |this, _window, _cx| {
|
||||||
this.width(px(520.))
|
this.width(px(520.))
|
||||||
.show_close(true)
|
.show_close(true)
|
||||||
.pb_4()
|
.pb_2()
|
||||||
.title("Preferences")
|
.title("Preferences")
|
||||||
.child(view.clone())
|
.child(view.clone())
|
||||||
});
|
});
|
||||||
@@ -341,6 +356,20 @@ impl Workspace {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn account_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let accounts = accounts::init(window, cx);
|
||||||
|
|
||||||
|
window.open_modal(cx, move |this, _window, _cx| {
|
||||||
|
this.width(px(520.))
|
||||||
|
.title("Continue with")
|
||||||
|
.show_close(false)
|
||||||
|
.keyboard(false)
|
||||||
|
.overlay_closable(false)
|
||||||
|
.pb_2()
|
||||||
|
.child(accounts.clone())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn theme_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn theme_selector(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
window.open_modal(cx, move |this, _window, cx| {
|
window.open_modal(cx, move |this, _window, cx| {
|
||||||
let registry = ThemeRegistry::global(cx);
|
let registry = ThemeRegistry::global(cx);
|
||||||
@@ -349,20 +378,22 @@ impl Workspace {
|
|||||||
this.width(px(520.))
|
this.width(px(520.))
|
||||||
.show_close(true)
|
.show_close(true)
|
||||||
.title("Select theme")
|
.title("Select theme")
|
||||||
.pb_4()
|
.pb_2()
|
||||||
.child(v_flex().gap_2().w_full().children({
|
.child(v_flex().gap_2().w_full().children({
|
||||||
let mut items = vec![];
|
let mut items = vec![];
|
||||||
|
|
||||||
for (ix, (path, theme)) in themes.iter().enumerate() {
|
for (ix, (path, theme)) in themes.iter().enumerate() {
|
||||||
items.push(
|
items.push(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
.id(ix)
|
||||||
.group("")
|
.group("")
|
||||||
.px_2()
|
.px_2()
|
||||||
.h_8()
|
.h_8()
|
||||||
.w_full()
|
.w_full()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
.bg(cx.theme().ghost_element_background)
|
||||||
|
.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1p5()
|
.gap_1p5()
|
||||||
@@ -485,12 +516,12 @@ impl Workspace {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(nostr.read(cx).creating(), |this| {
|
.when(nostr.read(cx).creating, |this| {
|
||||||
this.child(div().text_xs().text_color(cx.theme().text_muted).child(
|
this.child(div().text_xs().text_color(cx.theme().text_muted).child(
|
||||||
SharedString::from("Coop is creating a new identity for you..."),
|
SharedString::from("Coop is creating a new identity for you..."),
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.when(!nostr.read(cx).connected(), |this| {
|
.when(!nostr.read(cx).connected, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
@@ -503,7 +534,6 @@ impl Workspace {
|
|||||||
fn titlebar_right(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
|
fn titlebar_right(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let signer = nostr.read(cx).signer();
|
let signer = nostr.read(cx).signer();
|
||||||
let relay_list = nostr.read(cx).relay_list_state();
|
|
||||||
|
|
||||||
let chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let inbox_state = chat.read(cx).state(cx);
|
let inbox_state = chat.read(cx).state(cx);
|
||||||
@@ -633,7 +663,7 @@ impl Workspace {
|
|||||||
div()
|
div()
|
||||||
.text_xs()
|
.text_xs()
|
||||||
.text_color(cx.theme().text_muted)
|
.text_color(cx.theme().text_muted)
|
||||||
.map(|this| match relay_list {
|
.map(|this| match nostr.read(cx).relay_list_state {
|
||||||
RelayState::Checking => this
|
RelayState::Checking => this
|
||||||
.child(div().child(SharedString::from(
|
.child(div().child(SharedString::from(
|
||||||
"Fetching user's relay list...",
|
"Fetching user's relay list...",
|
||||||
@@ -652,7 +682,9 @@ impl Workspace {
|
|||||||
.tooltip("User's relay list")
|
.tooltip("User's relay list")
|
||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
.when(relay_list.configured(), |this| this.indicator())
|
.when(nostr.read(cx).relay_list_state.configured(), |this| {
|
||||||
|
this.indicator()
|
||||||
|
})
|
||||||
.dropdown_menu(move |this, _window, cx| {
|
.dropdown_menu(move |this, _window, cx| {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let urls = nostr.read(cx).read_only_relays(&pkey, cx);
|
let urls = nostr.read(cx).read_only_relays(&pkey, cx);
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ impl DeviceRegistry {
|
|||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Observe the NIP-65 state
|
// Observe the NIP-65 state
|
||||||
cx.observe(&nostr, |this, state, cx| {
|
cx.observe(&nostr, |this, state, cx| {
|
||||||
if state.read(cx).relay_list_state() == RelayState::Configured {
|
if state.read(cx).relay_list_state == RelayState::Configured {
|
||||||
this.get_announcement(cx);
|
this.get_announcement(cx);
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -477,7 +477,7 @@ impl DeviceRegistry {
|
|||||||
|
|
||||||
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
let write_relays = nostr.read(cx).write_relays(&public_key, cx);
|
||||||
|
|
||||||
let app_keys = nostr.read(cx).app_keys().clone();
|
let app_keys = nostr.read(cx).app_keys.clone();
|
||||||
let app_pubkey = app_keys.public_key();
|
let app_pubkey = app_keys.public_key();
|
||||||
|
|
||||||
let task: Task<Result<Option<Keys>, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<Option<Keys>, Error>> = cx.background_spawn(async move {
|
||||||
@@ -549,7 +549,7 @@ impl DeviceRegistry {
|
|||||||
/// Parse the response event for device keys from other devices
|
/// Parse the response event for device keys from other devices
|
||||||
fn extract_encryption(&mut self, event: Event, cx: &mut Context<Self>) {
|
fn extract_encryption(&mut self, event: Event, cx: &mut Context<Self>) {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let app_keys = nostr.read(cx).app_keys().clone();
|
let app_keys = nostr.read(cx).app_keys.clone();
|
||||||
|
|
||||||
let task: Task<Result<Keys, Error>> = cx.background_spawn(async move {
|
let task: Task<Result<Keys, Error>> = cx.background_spawn(async move {
|
||||||
let root_device = event
|
let root_device = event
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ pub const FIND_LIMIT: usize = 20;
|
|||||||
pub const NOSTR_CONNECT_TIMEOUT: u64 = 200;
|
pub const NOSTR_CONNECT_TIMEOUT: u64 = 200;
|
||||||
|
|
||||||
/// Default Nostr Connect relay
|
/// Default Nostr Connect relay
|
||||||
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app";
|
pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nip46.com";
|
||||||
|
|
||||||
/// Default subscription id for device gift wrap events
|
/// Default subscription id for device gift wrap events
|
||||||
pub const DEVICE_GIFTWRAP: &str = "device-gift-wraps";
|
pub const DEVICE_GIFTWRAP: &str = "device-gift-wraps";
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
use anyhow::{anyhow, Context as AnyhowContext, Error};
|
||||||
use common::config_dir;
|
use common::config_dir;
|
||||||
use gpui::{App, AppContext, Context, Entity, Global, SharedString, Task, Window};
|
use gpui::{App, AppContext, Context, Entity, EventEmitter, Global, SharedString, Task, Window};
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use nostr_lmdb::prelude::*;
|
use nostr_lmdb::prelude::*;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
@@ -42,6 +42,16 @@ struct GlobalNostrRegistry(Entity<NostrRegistry>);
|
|||||||
|
|
||||||
impl Global for GlobalNostrRegistry {}
|
impl Global for GlobalNostrRegistry {}
|
||||||
|
|
||||||
|
/// Signer event.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum SignerEvent {
|
||||||
|
/// A new signer has been set
|
||||||
|
Set,
|
||||||
|
|
||||||
|
/// An error occurred
|
||||||
|
Error(String),
|
||||||
|
}
|
||||||
|
|
||||||
/// Nostr Registry
|
/// Nostr Registry
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct NostrRegistry {
|
pub struct NostrRegistry {
|
||||||
@@ -54,22 +64,22 @@ pub struct NostrRegistry {
|
|||||||
/// Local public keys
|
/// Local public keys
|
||||||
npubs: Entity<Vec<PublicKey>>,
|
npubs: Entity<Vec<PublicKey>>,
|
||||||
|
|
||||||
/// App keys
|
|
||||||
///
|
|
||||||
/// Used for Nostr Connect and NIP-4e operations
|
|
||||||
app_keys: Keys,
|
|
||||||
|
|
||||||
/// Custom gossip implementation
|
/// Custom gossip implementation
|
||||||
gossip: Entity<Gossip>,
|
gossip: Entity<Gossip>,
|
||||||
|
|
||||||
|
/// App keys
|
||||||
|
///
|
||||||
|
/// Used for Nostr Connect and NIP-4e operations
|
||||||
|
pub app_keys: Keys,
|
||||||
|
|
||||||
/// Relay list state
|
/// Relay list state
|
||||||
relay_list_state: RelayState,
|
pub relay_list_state: RelayState,
|
||||||
|
|
||||||
/// Whether Coop is connected to all bootstrap relays
|
/// Whether Coop is connected to all bootstrap relays
|
||||||
connected: bool,
|
pub connected: bool,
|
||||||
|
|
||||||
/// Whether Coop is creating a new signer
|
/// Whether Coop is creating a new signer
|
||||||
creating: bool,
|
pub creating: bool,
|
||||||
|
|
||||||
/// Tasks for asynchronous operations
|
/// Tasks for asynchronous operations
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
tasks: Vec<Task<Result<(), Error>>>,
|
||||||
@@ -146,29 +156,9 @@ impl NostrRegistry {
|
|||||||
self.signer.clone()
|
self.signer.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the app keys
|
/// Get the npubs entity
|
||||||
pub fn app_keys(&self) -> &Keys {
|
pub fn npubs(&self) -> Entity<Vec<PublicKey>> {
|
||||||
&self.app_keys
|
self.npubs.clone()
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the connected status of the client
|
|
||||||
pub fn connected(&self) -> bool {
|
|
||||||
self.connected
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the creating status
|
|
||||||
pub fn creating(&self) -> bool {
|
|
||||||
self.creating
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the relay list state
|
|
||||||
pub fn relay_list_state(&self) -> RelayState {
|
|
||||||
self.relay_list_state.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get all relays for a given public key without ensuring connections
|
|
||||||
pub fn read_only_relays(&self, public_key: &PublicKey, cx: &App) -> Vec<SharedString> {
|
|
||||||
self.gossip.read(cx).read_only_relays(public_key)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the connected status of the client
|
/// Set the connected status of the client
|
||||||
@@ -304,10 +294,11 @@ impl NostrRegistry {
|
|||||||
Ok(public_keys) => match public_keys.is_empty() {
|
Ok(public_keys) => match public_keys.is_empty() {
|
||||||
true => {
|
true => {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.create_new_signer(cx);
|
this.create_identity(cx);
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
false => {
|
false => {
|
||||||
|
// TODO: auto login
|
||||||
npubs.update(cx, |this, cx| {
|
npubs.update(cx, |this, cx| {
|
||||||
this.extend(public_keys);
|
this.extend(public_keys);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
@@ -317,10 +308,11 @@ impl NostrRegistry {
|
|||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to get npubs: {e}");
|
log::error!("Failed to get npubs: {e}");
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.create_new_signer(cx);
|
this.create_identity(cx);
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -332,7 +324,7 @@ impl NostrRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new identity
|
/// Create a new identity
|
||||||
pub fn create_new_signer(&mut self, cx: &mut Context<Self>) {
|
fn create_identity(&mut self, cx: &mut Context<Self>) {
|
||||||
let client = self.client();
|
let client = self.client();
|
||||||
let keys = Keys::generate();
|
let keys = Keys::generate();
|
||||||
let async_keys = keys.clone();
|
let async_keys = keys.clone();
|
||||||
@@ -411,16 +403,17 @@ impl NostrRegistry {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the signer in keyring by username
|
/// Get the signer in keyring by username
|
||||||
pub fn get_signer(
|
pub fn get_signer(
|
||||||
&mut self,
|
&self,
|
||||||
username: &str,
|
public_key: &PublicKey,
|
||||||
cx: &mut Context<Self>,
|
cx: &App,
|
||||||
) -> Task<Result<Arc<dyn NostrSigner>, Error>> {
|
) -> Task<Result<Arc<dyn NostrSigner>, Error>> {
|
||||||
let app_keys = self.app_keys().clone();
|
let username = public_key.to_bech32().unwrap();
|
||||||
let read_credential = cx.read_credentials(username);
|
let app_keys = self.app_keys.clone();
|
||||||
|
let read_credential = cx.read_credentials(&username);
|
||||||
|
|
||||||
cx.spawn(async move |_this, _cx| {
|
cx.spawn(async move |_cx| {
|
||||||
let (_, secret) = read_credential
|
let (_, secret) = read_credential
|
||||||
.await
|
.await
|
||||||
.map_err(|_| anyhow!("Failed to get signer"))?
|
.map_err(|_| anyhow!("Failed to get signer"))?
|
||||||
@@ -439,7 +432,7 @@ impl NostrRegistry {
|
|||||||
let uri =
|
let uri =
|
||||||
NostrConnectUri::parse(&sec).map_err(|_| anyhow!("Failed to parse NIP-46 URI"))?;
|
NostrConnectUri::parse(&sec).map_err(|_| anyhow!("Failed to parse NIP-46 URI"))?;
|
||||||
|
|
||||||
let timeout = Duration::from_secs(120);
|
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||||
let nip46 = NostrConnect::new(uri, app_keys, timeout, None)?;
|
let nip46 = NostrConnect::new(uri, app_keys, timeout, None)?;
|
||||||
|
|
||||||
Ok(nip46.into_nostr_signer())
|
Ok(nip46.into_nostr_signer())
|
||||||
@@ -478,30 +471,37 @@ impl NostrRegistry {
|
|||||||
});
|
});
|
||||||
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
// set signer
|
match task.await {
|
||||||
let public_key = task.await?;
|
Ok(public_key) => {
|
||||||
|
|
||||||
// Update states
|
// Update states
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
|
// Add public key to npubs if not already present
|
||||||
this.npubs.update(cx, |this, cx| {
|
this.npubs.update(cx, |this, cx| {
|
||||||
if !this.contains(&public_key) {
|
if !this.contains(&public_key) {
|
||||||
this.push(public_key);
|
this.push(public_key);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.ensure_relay_list(cx);
|
|
||||||
})?;
|
|
||||||
|
|
||||||
|
// Ensure relay list for the user
|
||||||
|
this.ensure_relay_list(cx);
|
||||||
|
|
||||||
|
// Emit signer changed event
|
||||||
|
cx.emit(SignerEvent::Set);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.update(cx, |_this, cx| {
|
||||||
|
cx.emit(SignerEvent::Error(e.to_string()));
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a key signer to keyring
|
/// Add a key signer to keyring
|
||||||
pub fn add_key_signer(
|
pub fn add_key_signer(&mut self, keys: &Keys, cx: &mut Context<Self>) {
|
||||||
&mut self,
|
|
||||||
keys: &Keys,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Task<Result<(), Error>> {
|
|
||||||
let keys = keys.clone();
|
let keys = keys.clone();
|
||||||
let username = keys.public_key().to_bech32().unwrap();
|
let username = keys.public_key().to_bech32().unwrap();
|
||||||
let secret = keys.secret_key().to_secret_bytes();
|
let secret = keys.secret_key().to_secret_bytes();
|
||||||
@@ -509,25 +509,26 @@ impl NostrRegistry {
|
|||||||
// Write the credential to the keyring
|
// Write the credential to the keyring
|
||||||
let write_credential = cx.write_credentials(&username, &username, &secret);
|
let write_credential = cx.write_credentials(&username, &username, &secret);
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
match write_credential.await {
|
match write_credential.await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_signer(keys, cx);
|
this.set_signer(keys, cx);
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
Err(e) => return Err(anyhow!("Failed to write credential: {e}")),
|
Err(e) => {
|
||||||
|
this.update(cx, |_this, cx| {
|
||||||
|
cx.emit(SignerEvent::Error(e.to_string()));
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a nostr connect signer to keyring
|
/// Add a nostr connect signer to keyring
|
||||||
pub fn add_nip46_signer(
|
pub fn add_nip46_signer(&mut self, nip46: &NostrConnect, cx: &mut Context<Self>) {
|
||||||
&mut self,
|
|
||||||
nip46: &NostrConnect,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Task<Result<(), Error>> {
|
|
||||||
let nip46 = nip46.clone();
|
let nip46 = nip46.clone();
|
||||||
let async_nip46 = nip46.clone();
|
let async_nip46 = nip46.clone();
|
||||||
|
|
||||||
@@ -540,7 +541,7 @@ impl NostrRegistry {
|
|||||||
Ok((public_key, uri))
|
Ok((public_key, uri))
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||||
match task.await {
|
match task.await {
|
||||||
Ok((public_key, uri)) => {
|
Ok((public_key, uri)) => {
|
||||||
let username = public_key.to_bech32().unwrap();
|
let username = public_key.to_bech32().unwrap();
|
||||||
@@ -554,13 +555,22 @@ impl NostrRegistry {
|
|||||||
this.set_signer(nip46, cx);
|
this.set_signer(nip46, cx);
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
Err(e) => return Err(anyhow!("Failed to write credential: {e}")),
|
Err(e) => {
|
||||||
|
this.update(cx, |_this, cx| {
|
||||||
|
cx.emit(SignerEvent::Error(e.to_string()));
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => return Err(anyhow!("Failed to connect to the remote signer: {e}")),
|
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
|
this.update(cx, |_this, cx| {
|
||||||
|
cx.emit(SignerEvent::Error(e.to_string()));
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the state of the relay list
|
/// Set the state of the relay list
|
||||||
@@ -716,9 +726,14 @@ impl NostrRegistry {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get all relays for a given public key without ensuring connections
|
||||||
|
pub fn read_only_relays(&self, public_key: &PublicKey, cx: &App) -> Vec<SharedString> {
|
||||||
|
self.gossip.read(cx).read_only_relays(public_key)
|
||||||
|
}
|
||||||
|
|
||||||
/// Generate a direct nostr connection initiated by the client
|
/// Generate a direct nostr connection initiated by the client
|
||||||
pub fn nostr_connect(&self, relay: Option<RelayUrl>) -> (NostrConnect, NostrConnectUri) {
|
pub fn nostr_connect(&self, relay: Option<RelayUrl>) -> (NostrConnect, NostrConnectUri) {
|
||||||
let app_keys = self.app_keys();
|
let app_keys = self.app_keys.clone();
|
||||||
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT);
|
||||||
|
|
||||||
// Determine the relay will be used for Nostr Connect
|
// Determine the relay will be used for Nostr Connect
|
||||||
@@ -894,6 +909,8 @@ impl NostrRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<SignerEvent> for NostrRegistry {}
|
||||||
|
|
||||||
/// Get or create a new app keys
|
/// Get or create a new app keys
|
||||||
fn get_or_init_app_keys() -> Result<Keys, Error> {
|
fn get_or_init_app_keys() -> Result<Keys, Error> {
|
||||||
let dir = config_dir().join(".app_keys");
|
let dir = config_dir().join(".app_keys");
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ pub enum IconName {
|
|||||||
CloseCircle,
|
CloseCircle,
|
||||||
CloseCircleFill,
|
CloseCircleFill,
|
||||||
Copy,
|
Copy,
|
||||||
|
Device,
|
||||||
Door,
|
Door,
|
||||||
Ellipsis,
|
Ellipsis,
|
||||||
Emoji,
|
Emoji,
|
||||||
@@ -52,6 +53,7 @@ pub enum IconName {
|
|||||||
Relay,
|
Relay,
|
||||||
Reply,
|
Reply,
|
||||||
Refresh,
|
Refresh,
|
||||||
|
Scan,
|
||||||
Search,
|
Search,
|
||||||
Settings,
|
Settings,
|
||||||
Settings2,
|
Settings2,
|
||||||
@@ -102,6 +104,7 @@ impl IconNamed for IconName {
|
|||||||
Self::CloseCircle => "icons/close-circle.svg",
|
Self::CloseCircle => "icons/close-circle.svg",
|
||||||
Self::CloseCircleFill => "icons/close-circle-fill.svg",
|
Self::CloseCircleFill => "icons/close-circle-fill.svg",
|
||||||
Self::Copy => "icons/copy.svg",
|
Self::Copy => "icons/copy.svg",
|
||||||
|
Self::Device => "icons/device.svg",
|
||||||
Self::Door => "icons/door.svg",
|
Self::Door => "icons/door.svg",
|
||||||
Self::Ellipsis => "icons/ellipsis.svg",
|
Self::Ellipsis => "icons/ellipsis.svg",
|
||||||
Self::Emoji => "icons/emoji.svg",
|
Self::Emoji => "icons/emoji.svg",
|
||||||
@@ -120,6 +123,7 @@ impl IconNamed for IconName {
|
|||||||
Self::Relay => "icons/relay.svg",
|
Self::Relay => "icons/relay.svg",
|
||||||
Self::Reply => "icons/reply.svg",
|
Self::Reply => "icons/reply.svg",
|
||||||
Self::Refresh => "icons/refresh.svg",
|
Self::Refresh => "icons/refresh.svg",
|
||||||
|
Self::Scan => "icons/scan.svg",
|
||||||
Self::Search => "icons/search.svg",
|
Self::Search => "icons/search.svg",
|
||||||
Self::Settings => "icons/settings.svg",
|
Self::Settings => "icons/settings.svg",
|
||||||
Self::Settings2 => "icons/settings2.svg",
|
Self::Settings2 => "icons/settings2.svg",
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ impl RenderOnce for Modal {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let window_paddings = crate::root::window_paddings(window, cx);
|
let window_paddings = crate::root::window_paddings(window, cx);
|
||||||
let radius = (cx.theme().radius_lg * 2.).min(px(20.));
|
let radius = cx.theme().radius_lg;
|
||||||
|
|
||||||
let view_size = window.viewport_size()
|
let view_size = window.viewport_size()
|
||||||
- gpui::size(
|
- gpui::size(
|
||||||
@@ -360,8 +360,8 @@ impl RenderOnce for Modal {
|
|||||||
let y = self.margin_top.unwrap_or(view_size.height / 10.) + offset_top;
|
let y = self.margin_top.unwrap_or(view_size.height / 10.) + offset_top;
|
||||||
let x = bounds.center().x - self.width / 2.;
|
let x = bounds.center().x - self.width / 2.;
|
||||||
|
|
||||||
let mut padding_right = px(16.);
|
let mut padding_right = px(8.);
|
||||||
let mut padding_left = px(16.);
|
let mut padding_left = px(8.);
|
||||||
|
|
||||||
if let Some(pl) = self.style.padding.left {
|
if let Some(pl) = self.style.padding.left {
|
||||||
padding_left = pl.to_pixels(self.width.into(), window.rem_size());
|
padding_left = pl.to_pixels(self.width.into(), window.rem_size());
|
||||||
|
|||||||
Reference in New Issue
Block a user