Redesign for the v1 stable release #3
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -1318,7 +1318,6 @@ dependencies = [
|
|||||||
"title_bar",
|
"title_bar",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"ui",
|
"ui",
|
||||||
"webbrowser",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6093,10 +6092,12 @@ dependencies = [
|
|||||||
"flume",
|
"flume",
|
||||||
"gpui",
|
"gpui",
|
||||||
"log",
|
"log",
|
||||||
|
"nostr-connect",
|
||||||
"nostr-lmdb",
|
"nostr-lmdb",
|
||||||
"nostr-sdk",
|
"nostr-sdk",
|
||||||
"rustls",
|
"rustls",
|
||||||
"smol",
|
"smol",
|
||||||
|
"webbrowser",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ smallvec.workspace = true
|
|||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
oneshot.workspace = true
|
oneshot.workspace = true
|
||||||
webbrowser.workspace = true
|
|
||||||
|
|
||||||
indexset = "0.12.3"
|
indexset = "0.12.3"
|
||||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use gpui::{actions, App};
|
use gpui::{actions, App};
|
||||||
use key_store::{KeyItem, KeyStore};
|
use key_store::{KeyItem, KeyStore};
|
||||||
use nostr_connect::prelude::*;
|
|
||||||
use state::NostrRegistry;
|
use state::NostrRegistry;
|
||||||
|
|
||||||
// Sidebar actions
|
// 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) {
|
pub fn reset(cx: &mut App) {
|
||||||
let backend = KeyStore::global(cx).read(cx).backend();
|
let backend = KeyStore::global(cx).read(cx).backend();
|
||||||
let client = NostrRegistry::global(cx).read(cx).client();
|
let client = NostrRegistry::global(cx).read(cx).client();
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ use gpui::{
|
|||||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
||||||
StatefulInteractiveElement, Styled, Subscription, Window,
|
StatefulInteractiveElement, Styled, Subscription, Window,
|
||||||
};
|
};
|
||||||
use key_store::{Credential, KeyItem, KeyStore};
|
|
||||||
use nostr_connect::prelude::*;
|
use nostr_connect::prelude::*;
|
||||||
use person::PersonRegistry;
|
use person::PersonRegistry;
|
||||||
use relay_auth::RelayAuth;
|
use relay_auth::RelayAuth;
|
||||||
@@ -21,34 +20,23 @@ use title_bar::TitleBar;
|
|||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::dock_area::dock::DockPlacement;
|
use ui::dock_area::dock::DockPlacement;
|
||||||
use ui::dock_area::panel::PanelView;
|
|
||||||
use ui::dock_area::{ClosePanel, DockArea, DockItem};
|
use ui::dock_area::{ClosePanel, DockArea, DockItem};
|
||||||
use ui::modal::ModalButtonProps;
|
use ui::modal::ModalButtonProps;
|
||||||
use ui::popup_menu::PopupMenuExt;
|
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::{
|
use crate::actions::{
|
||||||
reset, DarkMode, KeyringPopup, Logout, Settings, Themes, ViewProfile, ViewRelays,
|
reset, DarkMode, KeyringPopup, Logout, Settings, Themes, ViewProfile, ViewRelays,
|
||||||
};
|
};
|
||||||
use crate::user::viewer;
|
use crate::user::viewer;
|
||||||
use crate::views::compose::compose_button;
|
use crate::views::compose::compose_button;
|
||||||
use crate::views::{onboarding, preferences, setup_relay, startup, welcome};
|
use crate::views::{preferences, setup_relay, welcome};
|
||||||
use crate::{login, new_identity, sidebar, user};
|
use crate::{login, sidebar, user};
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ChatSpace> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<ChatSpace> {
|
||||||
cx.new(|cx| ChatSpace::new(window, cx))
|
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)]
|
#[derive(Debug)]
|
||||||
pub struct ChatSpace {
|
pub struct ChatSpace {
|
||||||
/// App's Title Bar
|
/// App's Title Bar
|
||||||
@@ -61,20 +49,15 @@ pub struct ChatSpace {
|
|||||||
ready: bool,
|
ready: bool,
|
||||||
|
|
||||||
/// Event subscriptions
|
/// Event subscriptions
|
||||||
_subscriptions: SmallVec<[Subscription; 4]>,
|
_subscriptions: SmallVec<[Subscription; 3]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ChatSpace {
|
impl ChatSpace {
|
||||||
pub 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 chat = ChatRegistry::global(cx);
|
let chat = ChatRegistry::global(cx);
|
||||||
let keystore = KeyStore::global(cx);
|
|
||||||
|
|
||||||
let title_bar = cx.new(|_| TitleBar::new());
|
let title_bar = cx.new(|_| TitleBar::new());
|
||||||
let dock = cx.new(|cx| DockArea::new(window, cx));
|
let dock = cx.new(|cx| DockArea::new(window, cx));
|
||||||
|
|
||||||
let identity = nostr.read(cx).identity();
|
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
subscriptions.push(
|
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(
|
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| {
|
||||||
@@ -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 {
|
Self {
|
||||||
dock,
|
dock,
|
||||||
title_bar,
|
title_bar,
|
||||||
@@ -179,43 +123,29 @@ impl ChatSpace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_onboarding_layout(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn set_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>) {
|
|
||||||
let weak_dock = self.dock.downgrade();
|
let weak_dock = self.dock.downgrade();
|
||||||
|
|
||||||
let sidebar = Arc::new(sidebar::init(window, cx));
|
// Sidebar
|
||||||
let center = Arc::new(welcome::init(window, cx));
|
let left = DockItem::panel(Arc::new(sidebar::init(window, cx)));
|
||||||
|
|
||||||
let left = DockItem::panel(sidebar);
|
// Main workspace
|
||||||
let center = DockItem::split_with_sizes(
|
let center = DockItem::split_with_sizes(
|
||||||
Axis::Vertical,
|
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],
|
vec![None],
|
||||||
&weak_dock,
|
&weak_dock,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.ready = true;
|
// Update the dock layout
|
||||||
self.dock.update(cx, |this, cx| {
|
self.dock.update(cx, |this, cx| {
|
||||||
this.set_left_dock(left, Some(px(DEFAULT_SIDEBAR_WIDTH)), true, window, cx);
|
this.set_left_dock(left, Some(px(DEFAULT_SIDEBAR_WIDTH)), true, window, cx);
|
||||||
this.set_center(center, window, cx);
|
this.set_center(center, window, cx);
|
||||||
@@ -426,24 +356,6 @@ impl ChatSpace {
|
|||||||
Some(ids)
|
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 {
|
fn titlebar_left(&mut self, _window: &mut Window, cx: &Context<Self>) -> impl IntoElement {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
let chat = ChatRegistry::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 {
|
impl Render for ChatSpace {
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let modal_layer = Root::render_modal_layer(window, cx);
|
let modal_layer = Root::render_modal_layer(window, cx);
|
||||||
let notification_layer = Root::render_notification_layer(window, cx);
|
let notification_layer = Root::render_notification_layer(window, cx);
|
||||||
|
|
||||||
let left = self.titlebar_left(window, cx).into_any_element();
|
let left = self.titlebar_left(window, cx).into_any_element();
|
||||||
let right = self.titlebar_right(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
|
// Update title bar children
|
||||||
self.title_bar.update(cx, |this, _cx| {
|
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()
|
div()
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ use ui::input::{InputEvent, InputState, TextInput};
|
|||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::{v_flex, Disableable, StyledExt, WindowExtension};
|
use ui::{v_flex, Disableable, StyledExt, WindowExtension};
|
||||||
|
|
||||||
use crate::actions::CoopAuthUrlHandler;
|
|
||||||
|
|
||||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Login> {
|
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Login> {
|
||||||
cx.new(|cx| Login::new(window, cx))
|
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();
|
let mut signer = NostrConnect::new(uri, app_keys.clone(), timeout, None).unwrap();
|
||||||
|
|
||||||
// Handle auth url with the default browser
|
// Handle auth url with the default browser
|
||||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
// signer.auth_url_handler(CoopAuthUrlHandler);
|
||||||
|
|
||||||
// Start countdown
|
// Start countdown
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
cx.spawn_in(window, async move |this, cx| {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
pub mod compose;
|
pub mod compose;
|
||||||
pub mod onboarding;
|
|
||||||
pub mod preferences;
|
pub mod preferences;
|
||||||
pub mod screening;
|
pub mod screening;
|
||||||
pub mod setup_relay;
|
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::indicator::Indicator;
|
||||||
use ui::{h_flex, v_flex, Sizable, StyledExt, WindowExtension};
|
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> {
|
pub fn init(cre: Credential, window: &mut Window, cx: &mut App) -> Entity<Startup> {
|
||||||
cx.new(|cx| Startup::new(cre, window, cx))
|
cx.new(|cx| Startup::new(cre, window, cx))
|
||||||
@@ -129,7 +129,7 @@ impl Startup {
|
|||||||
let mut signer = NostrConnect::new(uri, keys, timeout, None).unwrap();
|
let mut signer = NostrConnect::new(uri, keys, timeout, None).unwrap();
|
||||||
|
|
||||||
// Handle auth url with the default browser
|
// Handle auth url with the default browser
|
||||||
signer.auth_url_handler(CoopAuthUrlHandler);
|
// signer.auth_url_handler(CoopAuthUrlHandler);
|
||||||
|
|
||||||
// Connect to the remote signer
|
// Connect to the remote signer
|
||||||
this._tasks.push(
|
this._tasks.push(
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ common = { path = "../common" }
|
|||||||
|
|
||||||
nostr-sdk.workspace = true
|
nostr-sdk.workspace = true
|
||||||
nostr-lmdb.workspace = true
|
nostr-lmdb.workspace = true
|
||||||
|
nostr-connect.workspace = true
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
flume.workspace = true
|
flume.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
webbrowser.workspace = true
|
||||||
|
|
||||||
rustls = "0.23"
|
rustls = "0.23"
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ use std::collections::HashSet;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Error;
|
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 gpui::{App, AppContext, Context, Entity, Global, Subscription, Task};
|
||||||
|
use nostr_connect::prelude::*;
|
||||||
use nostr_lmdb::NostrLmdb;
|
use nostr_lmdb::NostrLmdb;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
|
|
||||||
@@ -25,6 +26,10 @@ pub fn init(cx: &mut App) {
|
|||||||
|
|
||||||
/// Default timeout for subscription
|
/// Default timeout for subscription
|
||||||
pub const TIMEOUT: u64 = 3;
|
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
|
/// Default subscription id for gift wrap events
|
||||||
pub const GIFTWRAP_SUBSCRIPTION: &str = "giftwrap-events";
|
pub const GIFTWRAP_SUBSCRIPTION: &str = "giftwrap-events";
|
||||||
@@ -592,4 +597,56 @@ impl NostrRegistry {
|
|||||||
Ok(())
|
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