Redesign for the v1 stable release #3
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -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]]
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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<Result<()>> {
|
||||
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();
|
||||
|
||||
@@ -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<ChatSpace> {
|
||||
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>) -> Self {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> 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<Self>) {
|
||||
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<Self>) {
|
||||
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<Self>) {
|
||||
fn set_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
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<P>(panel: P, window: &mut Window, cx: &mut App)
|
||||
where
|
||||
P: PanelView,
|
||||
{
|
||||
if let Some(Some(root)) = window.root::<Root>() {
|
||||
if let Ok(chatspace) = root.read(cx).view().clone().downcast::<ChatSpace>() {
|
||||
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<Self>) -> 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<Self>) -> 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<Self>) -> 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()
|
||||
|
||||
@@ -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<Login> {
|
||||
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| {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
pub mod compose;
|
||||
pub mod onboarding;
|
||||
pub mod preferences;
|
||||
pub mod screening;
|
||||
pub mod setup_relay;
|
||||
|
||||
@@ -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> {
|
||||
Onboarding::new(window, cx)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum NostrConnectApp {
|
||||
Nsec(String),
|
||||
Amber(String),
|
||||
Aegis(String),
|
||||
}
|
||||
|
||||
impl NostrConnectApp {
|
||||
pub fn all() -> Vec<Self> {
|
||||
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<Arc<Image>>,
|
||||
|
||||
/// Panel
|
||||
name: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
|
||||
/// Background tasks
|
||||
_tasks: SmallVec<[Task<()>; 1]>,
|
||||
}
|
||||
|
||||
impl Onboarding {
|
||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|cx| Self::view(window, cx))
|
||||
}
|
||||
|
||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> 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<Self>,
|
||||
) {
|
||||
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<Self>) {
|
||||
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<Self>) -> impl IntoIterator<Item = impl IntoElement> {
|
||||
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<T>(&self, ix: usize, label: T, url: &str, cx: &Context<Self>) -> impl IntoElement
|
||||
where
|
||||
T: Into<SharedString>,
|
||||
{
|
||||
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<PanelEvent> 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<Self>) -> 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)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<Startup> {
|
||||
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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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<RelayUrl>) -> (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<Result<()>> {
|
||||
Box::pin(async move {
|
||||
webbrowser::open(auth_url.as_str())?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user