From ed403188f840376dc2d1c2a7b92a03ff12fe9ff5 Mon Sep 17 00:00:00 2001 From: reya Date: Mon, 19 Jan 2026 17:17:28 +0700 Subject: [PATCH] wip --- Cargo.lock | 3 +- crates/coop/Cargo.toml | 1 - crates/coop/src/actions.rs | 15 -- crates/coop/src/chatspace.rs | 178 ++------------ crates/coop/src/login/mod.rs | 4 +- crates/coop/src/views/mod.rs | 1 - crates/coop/src/views/onboarding.rs | 363 ---------------------------- crates/coop/src/views/startup.rs | 4 +- crates/state/Cargo.toml | 2 + crates/state/src/lib.rs | 59 ++++- 10 files changed, 88 insertions(+), 542 deletions(-) delete mode 100644 crates/coop/src/views/onboarding.rs diff --git a/Cargo.lock b/Cargo.lock index 1b0e8c4..6da7c4c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1318,7 +1318,6 @@ dependencies = [ "title_bar", "tracing-subscriber", "ui", - "webbrowser", ] [[package]] @@ -6093,10 +6092,12 @@ dependencies = [ "flume", "gpui", "log", + "nostr-connect", "nostr-lmdb", "nostr-sdk", "rustls", "smol", + "webbrowser", ] [[package]] diff --git a/crates/coop/Cargo.toml b/crates/coop/Cargo.toml index a437497..a5e5ca4 100644 --- a/crates/coop/Cargo.toml +++ b/crates/coop/Cargo.toml @@ -58,7 +58,6 @@ smallvec.workspace = true smol.workspace = true futures.workspace = true oneshot.workspace = true -webbrowser.workspace = true indexset = "0.12.3" tracing-subscriber = { version = "0.3.18", features = ["fmt"] } diff --git a/crates/coop/src/actions.rs b/crates/coop/src/actions.rs index d69eced..a6cbdf8 100644 --- a/crates/coop/src/actions.rs +++ b/crates/coop/src/actions.rs @@ -1,6 +1,5 @@ use gpui::{actions, App}; use key_store::{KeyItem, KeyStore}; -use nostr_connect::prelude::*; use state::NostrRegistry; // Sidebar actions @@ -21,20 +20,6 @@ actions!( ] ); -#[derive(Debug, Clone)] -pub struct CoopAuthUrlHandler; - -impl AuthUrlHandler for CoopAuthUrlHandler { - #[allow(mismatched_lifetime_syntaxes)] - fn on_auth_url(&self, auth_url: Url) -> BoxedFuture> { - Box::pin(async move { - log::info!("Received Auth URL: {auth_url}"); - webbrowser::open(auth_url.as_str())?; - Ok(()) - }) - } -} - pub fn reset(cx: &mut App) { let backend = KeyStore::global(cx).read(cx).backend(); let client = NostrRegistry::global(cx).read(cx).client(); diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs index 6f85925..6ae1e2b 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/chatspace.rs @@ -10,7 +10,6 @@ use gpui::{ InteractiveElement, IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window, }; -use key_store::{Credential, KeyItem, KeyStore}; use nostr_connect::prelude::*; use person::PersonRegistry; use relay_auth::RelayAuth; @@ -21,34 +20,23 @@ use title_bar::TitleBar; use ui::avatar::Avatar; use ui::button::{Button, ButtonVariants}; use ui::dock_area::dock::DockPlacement; -use ui::dock_area::panel::PanelView; use ui::dock_area::{ClosePanel, DockArea, DockItem}; use ui::modal::ModalButtonProps; use ui::popup_menu::PopupMenuExt; -use ui::{h_flex, v_flex, IconName, Root, Sizable, StyledExt, WindowExtension}; +use ui::{h_flex, v_flex, IconName, Root, Sizable, WindowExtension}; use crate::actions::{ reset, DarkMode, KeyringPopup, Logout, Settings, Themes, ViewProfile, ViewRelays, }; use crate::user::viewer; use crate::views::compose::compose_button; -use crate::views::{onboarding, preferences, setup_relay, startup, welcome}; -use crate::{login, new_identity, sidebar, user}; +use crate::views::{preferences, setup_relay, welcome}; +use crate::{login, sidebar, user}; pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| ChatSpace::new(window, cx)) } -pub fn login(window: &mut Window, cx: &mut App) { - let panel = login::init(window, cx); - ChatSpace::set_center_panel(panel, window, cx); -} - -pub fn new_account(window: &mut Window, cx: &mut App) { - let panel = new_identity::init(window, cx); - ChatSpace::set_center_panel(panel, window, cx); -} - #[derive(Debug)] pub struct ChatSpace { /// App's Title Bar @@ -61,20 +49,15 @@ pub struct ChatSpace { ready: bool, /// Event subscriptions - _subscriptions: SmallVec<[Subscription; 4]>, + _subscriptions: SmallVec<[Subscription; 3]>, } impl ChatSpace { - pub fn new(window: &mut Window, cx: &mut Context) -> Self { - let nostr = NostrRegistry::global(cx); + fn new(window: &mut Window, cx: &mut Context) -> Self { let chat = ChatRegistry::global(cx); - let keystore = KeyStore::global(cx); - let title_bar = cx.new(|_| TitleBar::new()); let dock = cx.new(|cx| DockArea::new(window, cx)); - let identity = nostr.read(cx).identity(); - let mut subscriptions = smallvec![]; subscriptions.push( @@ -84,50 +67,6 @@ impl ChatSpace { }), ); - subscriptions.push( - // Observe account entity changes - cx.observe_in(&identity, window, move |this, state, window, cx| { - if !this.ready && state.read(cx).has_public_key() { - this.set_default_layout(window, cx); - - // Load all chat room in the database if available - let chat = ChatRegistry::global(cx); - chat.update(cx, |this, cx| { - this.get_rooms(cx); - }); - }; - }), - ); - - subscriptions.push( - // Observe keystore entity changes - cx.observe_in(&keystore, window, move |_this, state, window, cx| { - if state.read(cx).initialized { - let backend = state.read(cx).backend(); - - cx.spawn_in(window, async move |this, cx| { - let result = backend - .read_credentials(&KeyItem::User.to_string(), cx) - .await; - - this.update_in(cx, |this, window, cx| { - match result { - Ok(Some((user, secret))) => { - let credential = Credential::new(user, secret); - this.set_startup_layout(credential, window, cx); - } - _ => { - this.set_onboarding_layout(window, cx); - } - }; - }) - .ok(); - }) - .detach(); - } - }), - ); - subscriptions.push( // Observe all events emitted by the chat registry cx.subscribe_in(&chat, window, move |this, chat, ev, window, cx| { @@ -171,6 +110,11 @@ impl ChatSpace { }), ); + // Set the default layout for app's dock + cx.defer_in(window, |this, window, cx| { + this.set_layout(window, cx); + }); + Self { dock, title_bar, @@ -179,43 +123,29 @@ impl ChatSpace { } } - fn set_onboarding_layout(&mut self, window: &mut Window, cx: &mut Context) { - let panel = Arc::new(onboarding::init(window, cx)); - let center = DockItem::panel(panel); - - self.dock.update(cx, |this, cx| { - this.reset(window, cx); - this.set_center(center, window, cx); - }); - } - - fn set_startup_layout(&mut self, cre: Credential, window: &mut Window, cx: &mut Context) { - let panel = Arc::new(startup::init(cre, window, cx)); - let center = DockItem::panel(panel); - - self.dock.update(cx, |this, cx| { - this.reset(window, cx); - this.set_center(center, window, cx); - }); - } - - fn set_default_layout(&mut self, window: &mut Window, cx: &mut Context) { + fn set_layout(&mut self, window: &mut Window, cx: &mut Context) { let weak_dock = self.dock.downgrade(); - let sidebar = Arc::new(sidebar::init(window, cx)); - let center = Arc::new(welcome::init(window, cx)); + // Sidebar + let left = DockItem::panel(Arc::new(sidebar::init(window, cx))); - let left = DockItem::panel(sidebar); + // Main workspace let center = DockItem::split_with_sizes( Axis::Vertical, - vec![DockItem::tabs(vec![center], None, &weak_dock, window, cx)], + vec![DockItem::tabs( + vec![Arc::new(welcome::init(window, cx))], + None, + &weak_dock, + window, + cx, + )], vec![None], &weak_dock, window, cx, ); - self.ready = true; + // Update the dock layout self.dock.update(cx, |this, cx| { this.set_left_dock(left, Some(px(DEFAULT_SIDEBAR_WIDTH)), true, window, cx); this.set_center(center, window, cx); @@ -426,24 +356,6 @@ impl ChatSpace { Some(ids) } - fn set_center_panel

(panel: P, window: &mut Window, cx: &mut App) - where - P: PanelView, - { - if let Some(Some(root)) = window.root::() { - if let Ok(chatspace) = root.read(cx).view().clone().downcast::() { - let panel = Arc::new(panel); - let center = DockItem::panel(panel); - - chatspace.update(cx, |this, cx| { - this.dock.update(cx, |this, cx| { - this.set_center(center, window, cx); - }); - }); - } - } - } - fn titlebar_left(&mut self, _window: &mut Window, cx: &Context) -> impl IntoElement { let nostr = NostrRegistry::global(cx); let chat = ChatRegistry::global(cx); @@ -569,62 +481,18 @@ impl ChatSpace { ) }) } - - fn titlebar_center(&mut self, cx: &mut Context) -> impl IntoElement { - let entity = cx.entity().downgrade(); - let panel = self.dock.read(cx).items.view(); - let title = panel.title(cx); - let id = panel.panel_id(cx); - - if id == "Onboarding" { - return div(); - }; - - h_flex() - .flex_1() - .w_full() - .justify_center() - .text_center() - .font_semibold() - .text_sm() - .child( - div().flex_1().child( - Button::new("back") - .icon(IconName::ArrowLeft) - .small() - .ghost_alt() - .rounded() - .on_click(move |_ev, window, cx| { - entity - .update(cx, |this, cx| { - this.set_onboarding_layout(window, cx); - }) - .expect("Entity has been released"); - }), - ), - ) - .child(div().flex_1().child(title)) - .child(div().flex_1()) - } } impl Render for ChatSpace { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let modal_layer = Root::render_modal_layer(window, cx); let notification_layer = Root::render_notification_layer(window, cx); - let left = self.titlebar_left(window, cx).into_any_element(); let right = self.titlebar_right(window, cx).into_any_element(); - let center = self.titlebar_center(cx).into_any_element(); - let single_panel = self.dock.read(cx).items.panel_ids(cx).is_empty(); // Update title bar children self.title_bar.update(cx, |this, _cx| { - if single_panel { - this.set_children(vec![center]); - } else { - this.set_children(vec![left, right]); - } + this.set_children(vec![left, right]); }); div() diff --git a/crates/coop/src/login/mod.rs b/crates/coop/src/login/mod.rs index b93ad3c..a62e42e 100644 --- a/crates/coop/src/login/mod.rs +++ b/crates/coop/src/login/mod.rs @@ -18,8 +18,6 @@ use ui::input::{InputEvent, InputState, TextInput}; use ui::notification::Notification; use ui::{v_flex, Disableable, StyledExt, WindowExtension}; -use crate::actions::CoopAuthUrlHandler; - pub fn init(window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Login::new(window, cx)) } @@ -120,7 +118,7 @@ impl Login { let mut signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap(); // Handle auth url with the default browser - signer.auth_url_handler(CoopAuthUrlHandler); + // signer.auth_url_handler(CoopAuthUrlHandler); // Start countdown cx.spawn_in(window, async move |this, cx| { diff --git a/crates/coop/src/views/mod.rs b/crates/coop/src/views/mod.rs index 2e6c806..2b146b7 100644 --- a/crates/coop/src/views/mod.rs +++ b/crates/coop/src/views/mod.rs @@ -1,5 +1,4 @@ pub mod compose; -pub mod onboarding; pub mod preferences; pub mod screening; pub mod setup_relay; diff --git a/crates/coop/src/views/onboarding.rs b/crates/coop/src/views/onboarding.rs deleted file mode 100644 index ba29e34..0000000 --- a/crates/coop/src/views/onboarding.rs +++ /dev/null @@ -1,363 +0,0 @@ -use std::sync::Arc; -use std::time::Duration; - -use common::{TextUtils, CLIENT_NAME, NOSTR_CONNECT_RELAY, NOSTR_CONNECT_TIMEOUT}; -use gpui::prelude::FluentBuilder; -use gpui::{ - div, img, px, relative, svg, AnyElement, App, AppContext, Context, Entity, EventEmitter, - FocusHandle, Focusable, Image, InteractiveElement, IntoElement, ParentElement, Render, - SharedString, StatefulInteractiveElement, Styled, Task, Window, -}; -use key_store::{KeyItem, KeyStore}; -use nostr_connect::prelude::*; -use smallvec::{smallvec, SmallVec}; -use state::NostrRegistry; -use theme::ActiveTheme; -use ui::button::{Button, ButtonVariants}; -use ui::dock_area::panel::{Panel, PanelEvent}; -use ui::notification::Notification; -use ui::{divider, h_flex, v_flex, Icon, IconName, Sizable, StyledExt, WindowExtension}; - -use crate::chatspace::{self}; - -pub fn init(window: &mut Window, cx: &mut App) -> Entity { - Onboarding::new(window, cx) -} - -#[derive(Debug, Clone)] -pub enum NostrConnectApp { - Nsec(String), - Amber(String), - Aegis(String), -} - -impl NostrConnectApp { - pub fn all() -> Vec { - vec![ - NostrConnectApp::Nsec("https://nsec.app".to_string()), - NostrConnectApp::Amber("https://github.com/greenart7c3/Amber".to_string()), - NostrConnectApp::Aegis("https://github.com/ZharlieW/Aegis".to_string()), - ] - } - - pub fn url(&self) -> &str { - match self { - Self::Nsec(url) | Self::Amber(url) | Self::Aegis(url) => url, - } - } - - pub fn as_str(&self) -> String { - match self { - NostrConnectApp::Nsec(_) => "nsec.app (Desktop)".into(), - NostrConnectApp::Amber(_) => "Amber (Android)".into(), - NostrConnectApp::Aegis(_) => "Aegis (iOS)".into(), - } - } -} - -pub struct Onboarding { - app_keys: Keys, - qr_code: Option>, - - /// Panel - name: SharedString, - focus_handle: FocusHandle, - - /// Background tasks - _tasks: SmallVec<[Task<()>; 1]>, -} - -impl Onboarding { - pub fn new(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| Self::view(window, cx)) - } - - fn view(window: &mut Window, cx: &mut Context) -> Self { - let app_keys = Keys::generate(); - let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT); - - let relay = RelayUrl::parse(NOSTR_CONNECT_RELAY).unwrap(); - let uri = NostrConnectUri::client(app_keys.public_key(), vec![relay], CLIENT_NAME); - let qr_code = uri.to_string().to_qr(); - - // NIP46: https://github.com/nostr-protocol/nips/blob/master/46.md - // - // Direct connection initiated by the client - let signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap(); - - let mut tasks = smallvec![]; - - tasks.push( - // Wait for nostr connect - cx.spawn_in(window, async move |this, cx| { - let result = signer.bunker_uri().await; - - this.update_in(cx, |this, window, cx| { - match result { - Ok(uri) => { - this.save_connection(&uri, window, cx); - this.connect(signer, cx); - } - Err(e) => { - window.push_notification(Notification::error(e.to_string()), cx); - } - }; - }) - .ok(); - }), - ); - - Self { - qr_code, - app_keys, - name: "Onboarding".into(), - focus_handle: cx.focus_handle(), - _tasks: tasks, - } - } - - fn save_connection( - &mut self, - uri: &NostrConnectUri, - window: &mut Window, - cx: &mut Context, - ) { - let keystore = KeyStore::global(cx).read(cx).backend(); - let username = self.app_keys.public_key().to_hex(); - let secret = self.app_keys.secret_key().to_secret_bytes(); - let mut clean_uri = uri.to_string(); - - // Clear the secret parameter in the URI if it exists - if let Some(s) = uri.secret() { - clean_uri = clean_uri.replace(s, ""); - } - - cx.spawn_in(window, async move |this, cx| { - let user_url = KeyItem::User.to_string(); - let bunker_url = KeyItem::Bunker.to_string(); - let user_password = clean_uri.into_bytes(); - - // Write bunker uri to keyring for further connection - if let Err(e) = keystore - .write_credentials(&user_url, "bunker", &user_password, cx) - .await - { - this.update_in(cx, |_, window, cx| { - window.push_notification(e.to_string(), cx); - }) - .ok(); - } - - // Write the app keys for further connection - if let Err(e) = keystore - .write_credentials(&bunker_url, &username, &secret, cx) - .await - { - this.update_in(cx, |_, window, cx| { - window.push_notification(e.to_string(), cx); - }) - .ok(); - } - }) - .detach(); - } - - fn connect(&mut self, signer: NostrConnect, cx: &mut Context) { - let nostr = NostrRegistry::global(cx); - let client = nostr.read(cx).client(); - - cx.background_spawn(async move { - client.set_signer(signer).await; - }) - .detach(); - } - - fn render_apps(&self, cx: &Context) -> impl IntoIterator { - let all_apps = NostrConnectApp::all(); - let mut items = Vec::with_capacity(all_apps.len()); - - for (ix, item) in all_apps.into_iter().enumerate() { - items.push(self.render_app(ix, item.as_str(), item.url(), cx)); - } - - items - } - - fn render_app(&self, ix: usize, label: T, url: &str, cx: &Context) -> impl IntoElement - where - T: Into, - { - div() - .id(ix) - .flex_1() - .rounded(cx.theme().radius) - .py_0p5() - .px_2() - .bg(cx.theme().ghost_element_background_alt) - .child(label.into()) - .on_click({ - let url = url.to_owned(); - move |_e, _window, cx| { - cx.open_url(&url); - } - }) - } -} - -impl Panel for Onboarding { - fn panel_id(&self) -> SharedString { - self.name.clone() - } - - fn title(&self, _cx: &App) -> AnyElement { - self.name.clone().into_any_element() - } -} - -impl EventEmitter for Onboarding {} - -impl Focusable for Onboarding { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for Onboarding { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - h_flex() - .size_full() - .child( - v_flex() - .flex_1() - .h_full() - .gap_10() - .items_center() - .justify_center() - .child( - v_flex() - .items_center() - .justify_center() - .gap_4() - .child( - svg() - .path("brand/coop.svg") - .size_16() - .text_color(cx.theme().elevated_surface_background), - ) - .child( - div() - .text_center() - .child( - div() - .text_xl() - .font_semibold() - .line_height(relative(1.3)) - .child(SharedString::from("Welcome to Coop")), - ) - .child(div().text_color(cx.theme().text_muted).child( - SharedString::from("Chat Freely, Stay Private on Nostr."), - )), - ), - ) - .child( - v_flex() - .w_80() - .gap_3() - .child( - Button::new("continue_btn") - .icon(Icon::new(IconName::ArrowRight)) - .label(SharedString::from("Start Messaging on Nostr")) - .primary() - .large() - .bold() - .reverse() - .on_click(cx.listener(move |_, _, window, cx| { - chatspace::new_account(window, cx); - })), - ) - .child( - h_flex() - .my_1() - .gap_1() - .child(divider(cx)) - .child(div().text_sm().text_color(cx.theme().text_muted).child( - SharedString::from( - "Already have an account? Continue with", - ), - )) - .child(divider(cx)), - ) - .child( - Button::new("key") - .label("Secret Key or Bunker") - .large() - .ghost_alt() - .on_click(cx.listener(move |_, _, window, cx| { - chatspace::login(window, cx); - })), - ), - ), - ) - .child( - div() - .relative() - .p_2() - .flex_1() - .h_full() - .rounded(cx.theme().radius_lg) - .child( - v_flex() - .size_full() - .justify_center() - .bg(cx.theme().surface_background) - .rounded(cx.theme().radius_lg) - .child( - v_flex() - .gap_5() - .items_center() - .justify_center() - .when_some(self.qr_code.as_ref(), |this, qr| { - this.child( - img(qr.clone()) - .size(px(256.)) - .rounded(cx.theme().radius_lg) - .when(cx.theme().shadow, |this| this.shadow_lg()) - .border_1() - .border_color(cx.theme().element_active), - ) - }) - .child( - v_flex() - .justify_center() - .items_center() - .text_center() - .child( - div() - .font_semibold() - .line_height(relative(1.3)) - .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", - )), - ) - .child( - h_flex() - .mt_2() - .gap_1() - .text_xs() - .justify_center() - .children(self.render_apps(cx)), - ), - ), - ), - ), - ) - } -} diff --git a/crates/coop/src/views/startup.rs b/crates/coop/src/views/startup.rs index 4a77d82..80b14de 100644 --- a/crates/coop/src/views/startup.rs +++ b/crates/coop/src/views/startup.rs @@ -20,7 +20,7 @@ use ui::dock_area::panel::{Panel, PanelEvent}; use ui::indicator::Indicator; use ui::{h_flex, v_flex, Sizable, StyledExt, WindowExtension}; -use crate::actions::{reset, CoopAuthUrlHandler}; +use crate::actions::reset; pub fn init(cre: Credential, window: &mut Window, cx: &mut App) -> Entity { cx.new(|cx| Startup::new(cre, window, cx)) @@ -129,7 +129,7 @@ impl Startup { let mut signer = NostrConnect::new(uri, keys, timeout, None).unwrap(); // Handle auth url with the default browser - signer.auth_url_handler(CoopAuthUrlHandler); + // signer.auth_url_handler(CoopAuthUrlHandler); // Connect to the remote signer this._tasks.push( diff --git a/crates/state/Cargo.toml b/crates/state/Cargo.toml index 51ac44e..64dcec8 100644 --- a/crates/state/Cargo.toml +++ b/crates/state/Cargo.toml @@ -9,11 +9,13 @@ common = { path = "../common" } nostr-sdk.workspace = true nostr-lmdb.workspace = true +nostr-connect.workspace = true gpui.workspace = true smol.workspace = true flume.workspace = true log.workspace = true anyhow.workspace = true +webbrowser.workspace = true rustls = "0.23" diff --git a/crates/state/src/lib.rs b/crates/state/src/lib.rs index 201027c..4030f5a 100644 --- a/crates/state/src/lib.rs +++ b/crates/state/src/lib.rs @@ -2,8 +2,9 @@ use std::collections::HashSet; use std::time::Duration; use anyhow::Error; -use common::{config_dir, BOOTSTRAP_RELAYS, SEARCH_RELAYS}; +use common::{config_dir, BOOTSTRAP_RELAYS, CLIENT_NAME, SEARCH_RELAYS}; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task}; +use nostr_connect::prelude::*; use nostr_lmdb::NostrLmdb; use nostr_sdk::prelude::*; @@ -25,6 +26,10 @@ pub fn init(cx: &mut App) { /// Default timeout for subscription pub const TIMEOUT: u64 = 3; +/// Default timeout for Nostr Connect +pub const NOSTR_CONNECT_TIMEOUT: u64 = 200; +/// Default Nostr Connect relay +pub const NOSTR_CONNECT_RELAY: &str = "wss://relay.nsec.app"; /// Default subscription id for gift wrap events pub const GIFTWRAP_SUBSCRIPTION: &str = "giftwrap-events"; @@ -592,4 +597,56 @@ impl NostrRegistry { Ok(()) })); } + + /// Store a connection for future uses + pub fn persit_connection(&mut self, uri: NostrConnectUri, cx: &mut App) { + let client = self.client(); + let rng_keys = Keys::generate(); + + self.tasks.push(cx.background_spawn(async move { + // Construct the event for application-specific data + let event = EventBuilder::new(Kind::ApplicationSpecificData, uri.to_string()) + .tag(Tag::identifier("coop:account")) + .sign(&rng_keys) + .await?; + + // Store the event in the database + client.database().save_event(&event).await?; + + Ok(()) + })); + } + + /// Generate a direct nostr connection initiated by the client + pub fn client_connect(&self, relay: Option) -> (NostrConnect, NostrConnectUri) { + let app_keys = self.app_keys(); + let timeout = Duration::from_secs(NOSTR_CONNECT_TIMEOUT); + + // Determine the relay will be used for Nostr Connect + let relay = match relay { + Some(relay) => relay, + None => 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 signer = NostrConnect::new(uri.clone(), app_keys.clone(), timeout, None).unwrap(); + + (signer, uri) + } +} + +#[derive(Debug, Clone)] +pub struct CoopAuthUrlHandler; + +impl AuthUrlHandler for CoopAuthUrlHandler { + #[allow(mismatched_lifetime_syntaxes)] + fn on_auth_url(&self, auth_url: Url) -> BoxedFuture> { + Box::pin(async move { + webbrowser::open(auth_url.as_str())?; + Ok(()) + }) + } }