diff --git a/Cargo.lock b/Cargo.lock index 46eac26..6144faa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "account" +version = "0.0.0" +dependencies = [ + "anyhow", + "common", + "global", + "gpui", + "log", + "nostr-sdk", + "smallvec", + "smol", + "ui", +] + [[package]] name = "addr2line" version = "0.24.2" @@ -1127,7 +1142,6 @@ version = "0.0.0" dependencies = [ "anyhow", "chrono", - "dirs 5.0.1", "global", "gpui", "itertools 0.13.0", @@ -1176,6 +1190,7 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" name = "coop" version = "0.1.4" dependencies = [ + "account", "anyhow", "chats", "common", @@ -1184,6 +1199,7 @@ dependencies = [ "global", "gpui", "itertools 0.13.0", + "keyring", "log", "nostr-connect", "nostr-sdk", @@ -1413,6 +1429,35 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" +[[package]] +name = "dbus" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +dependencies = [ + "libc", + "libdbus-sys", + "winapi", +] + +[[package]] +name = "dbus-secret-service" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42a16374481d92aed73ae45b1f120207d8e71d24fb89f357fadbd8f946fd84b" +dependencies = [ + "aes", + "block-padding", + "cbc", + "dbus", + "futures-util", + "hkdf", + "num", + "once_cell", + "rand 0.8.5", + "sha2", +] + [[package]] name = "derive_more" version = "0.99.19" @@ -2962,6 +3007,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keyring" +version = "4.0.0-rc.1" +source = "git+https://github.com/hwchen/keyring-rs#9d1b02ff4c9fd1ff125c71f252c14b9ed7313fcb" +dependencies = [ + "byteorder", + "dbus-secret-service", + "log", + "security-framework", + "windows-sys 0.59.0", +] + [[package]] name = "khronos-egl" version = "6.0.0" @@ -3009,6 +3066,15 @@ version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +[[package]] +name = "libdbus-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +dependencies = [ + "pkg-config", +] + [[package]] name = "libfuzzer-sys" version = "0.4.9" @@ -3378,7 +3444,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "nostr" version = "0.40.0" -source = "git+https://github.com/reyamir/nostr?branch=feat%2Fimprove-nip17#a81679d648f55f2250feb8b88d0637cb28648d16" +source = "git+https://github.com/rust-nostr/nostr#942d0b07844071188ce81fa70ffac7c7d389c15a" dependencies = [ "aes", "base64", @@ -3403,7 +3469,7 @@ dependencies = [ [[package]] name = "nostr-connect" version = "0.40.0" -source = "git+https://github.com/reyamir/nostr?branch=feat%2Fimprove-nip17#a81679d648f55f2250feb8b88d0637cb28648d16" +source = "git+https://github.com/rust-nostr/nostr#942d0b07844071188ce81fa70ffac7c7d389c15a" dependencies = [ "async-utility", "nostr", @@ -3415,7 +3481,7 @@ dependencies = [ [[package]] name = "nostr-database" version = "0.40.0" -source = "git+https://github.com/reyamir/nostr?branch=feat%2Fimprove-nip17#a81679d648f55f2250feb8b88d0637cb28648d16" +source = "git+https://github.com/rust-nostr/nostr#942d0b07844071188ce81fa70ffac7c7d389c15a" dependencies = [ "flatbuffers", "lru", @@ -3426,7 +3492,7 @@ dependencies = [ [[package]] name = "nostr-lmdb" version = "0.40.0" -source = "git+https://github.com/reyamir/nostr?branch=feat%2Fimprove-nip17#a81679d648f55f2250feb8b88d0637cb28648d16" +source = "git+https://github.com/rust-nostr/nostr#942d0b07844071188ce81fa70ffac7c7d389c15a" dependencies = [ "async-utility", "heed", @@ -3439,7 +3505,7 @@ dependencies = [ [[package]] name = "nostr-relay-pool" version = "0.40.0" -source = "git+https://github.com/reyamir/nostr?branch=feat%2Fimprove-nip17#a81679d648f55f2250feb8b88d0637cb28648d16" +source = "git+https://github.com/rust-nostr/nostr#942d0b07844071188ce81fa70ffac7c7d389c15a" dependencies = [ "async-utility", "async-wsocket", @@ -3456,7 +3522,7 @@ dependencies = [ [[package]] name = "nostr-sdk" version = "0.40.0" -source = "git+https://github.com/reyamir/nostr?branch=feat%2Fimprove-nip17#a81679d648f55f2250feb8b88d0637cb28648d16" +source = "git+https://github.com/rust-nostr/nostr#942d0b07844071188ce81fa70ffac7c7d389c15a" dependencies = [ "async-utility", "nostr", diff --git a/Cargo.toml b/Cargo.toml index 11282a2..732d90c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,9 @@ gpui = { git = "https://github.com/zed-industries/zed" } reqwest_client = { git = "https://github.com/zed-industries/zed" } # Nostr -nostr-relay-builder = { git = "https://github.com/reyamir/nostr", branch = "feat/improve-nip17" } -nostr-connect = { git = "https://github.com/reyamir/nostr", branch = "feat/improve-nip17" } -nostr-sdk = { git = "https://github.com/reyamir/nostr", branch = "feat/improve-nip17", features = [ +nostr-relay-builder = { git = "https://github.com/rust-nostr/nostr" } +nostr-connect = { git = "https://github.com/rust-nostr/nostr" } +nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [ "lmdb", "nip96", "nip59", @@ -35,6 +35,7 @@ anyhow = "1.0.44" smallvec = "1.14.0" rust-embed = "8.5.0" log = "0.4" +keyring = { git = "https://github.com/hwchen/keyring-rs" } [profile.release] strip = true diff --git a/assets/icons/arrow-left.svg b/assets/icons/arrow-left.svg new file mode 100644 index 0000000..d0b5d88 --- /dev/null +++ b/assets/icons/arrow-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/arrow-right.svg b/assets/icons/arrow-right.svg new file mode 100644 index 0000000..05ef2da --- /dev/null +++ b/assets/icons/arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/account/Cargo.toml b/crates/account/Cargo.toml new file mode 100644 index 0000000..ad9cff7 --- /dev/null +++ b/crates/account/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "account" +version = "0.0.0" +edition = "2021" +publish = false + +[dependencies] +ui = { path = "../ui" } +common = { path = "../common" } +global = { path = "../global" } + +gpui.workspace = true +nostr-sdk.workspace = true +anyhow.workspace = true +smol.workspace = true +smallvec.workspace = true +log.workspace = true diff --git a/crates/account/src/lib.rs b/crates/account/src/lib.rs new file mode 100644 index 0000000..3113a55 --- /dev/null +++ b/crates/account/src/lib.rs @@ -0,0 +1,166 @@ +use std::time::Duration; + +use anyhow::Error; +use common::profile::NostrProfile; +use global::{ + constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID}, + get_client, +}; +use gpui::{App, AppContext, Context, Entity, Global, Task, Window}; +use nostr_sdk::prelude::*; +use ui::{notification::Notification, ContextModal}; + +struct GlobalAccount(Entity); + +impl Global for GlobalAccount {} + +pub fn init(cx: &mut App) { + Account::set_global(cx.new(|_| Account { profile: None }), cx); +} + +#[derive(Debug, Clone)] +pub struct Account { + pub profile: Option, +} + +impl Account { + pub fn global(cx: &App) -> Entity { + cx.global::().0.clone() + } + + pub fn set_global(account: Entity, cx: &mut App) { + cx.set_global(GlobalAccount(account)); + } + + pub fn login(&mut self, signer: S, window: &mut Window, cx: &mut Context) + where + S: NostrSigner + 'static, + { + let task: Task> = cx.background_spawn(async move { + let client = get_client(); + // Use user's signer for main signer + _ = client.set_signer(signer).await; + + // Verify nostr signer and get public key + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + + // Fetch user's metadata + let metadata = client + .fetch_metadata(public_key, Duration::from_secs(2)) + .await? + .unwrap_or_default(); + + Ok(NostrProfile::new(public_key, metadata)) + }); + + cx.spawn_in(window, |this, mut cx| async move { + match task.await { + Ok(profile) => { + cx.update(|_, cx| { + this.update(cx, |this, cx| { + this.profile = Some(profile); + this.subscribe(cx); + cx.notify(); + }) + }) + .ok(); + } + Err(e) => { + cx.update(|window, cx| { + window.push_notification(Notification::error(e.to_string()), cx) + }) + .ok(); + } + } + }) + .detach(); + } + + pub fn new_account(&mut self, metadata: Metadata, window: &mut Window, cx: &mut Context) { + let client = get_client(); + let keys = Keys::generate(); + + let task: Task> = cx.background_spawn(async move { + let public_key = keys.public_key(); + // Update signer + client.set_signer(keys).await; + // Set metadata + client.set_metadata(&metadata).await?; + + Ok(NostrProfile::new(public_key, metadata)) + }); + + cx.spawn_in(window, |this, mut cx| async move { + if let Ok(profile) = task.await { + cx.update(|_, cx| { + this.update(cx, |this, cx| { + this.profile = Some(profile); + this.subscribe(cx); + cx.notify(); + }) + }) + .ok(); + } else { + cx.update(|window, cx| { + window.push_notification(Notification::error("Failed to create account."), cx) + }) + .ok(); + } + }) + .detach(); + } + + pub fn subscribe(&self, cx: &Context) { + let Some(profile) = self.profile.as_ref() else { + return; + }; + + let client = get_client(); + let user = profile.public_key; + let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); + + // Create a contact list filter + let contacts = Filter::new().kind(Kind::ContactList).author(user).limit(1); + + // Create a user's data filter + let data = Filter::new() + .author(user) + .since(Timestamp::now()) + .kinds(vec![ + Kind::Metadata, + Kind::ContactList, + Kind::InboxRelays, + Kind::RelayList, + ]); + + // Create a filter for getting all gift wrapped events send to current user + let msg = Filter::new().kind(Kind::GiftWrap).pubkey(user); + + // Create a filter to continuously receive new messages. + let new_msg = Filter::new().kind(Kind::GiftWrap).pubkey(user).limit(0); + + let task: Task> = cx.background_spawn(async move { + // Only subscribe to the latest contact list + client.subscribe(contacts, Some(opts)).await?; + + // Continuously receive new user's data since now + client.subscribe(data, None).await?; + + let sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID); + client.subscribe_with_id(sub_id, msg, Some(opts)).await?; + + let sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID); + client.subscribe_with_id(sub_id, new_msg, None).await?; + + Ok(()) + }); + + cx.spawn(|_, _| async move { + if let Err(e) = task.await { + log::error!("Error: {}", e); + } + }) + .detach(); + } +} diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index a7fd1b4..4b73ad8 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -13,6 +13,7 @@ ui = { path = "../ui" } common = { path = "../common" } global = { path = "../global" } chats = { path = "../chats" } +account = { path = "../account" } gpui.workspace = true reqwest_client.workspace = true @@ -29,7 +30,8 @@ log.workspace = true smallvec.workspace = true smol.workspace = true oneshot.workspace = true +keyring.workspace = true rustls = "0.23.23" -futures= "0.3" +futures = "0.3" tracing-subscriber = { version = "0.3.18", features = ["fmt"] } diff --git a/crates/app/src/views/app.rs b/crates/app/src/chat_space.rs similarity index 61% rename from crates/app/src/views/app.rs rename to crates/app/src/chat_space.rs index c627d30..4868d48 100644 --- a/crates/app/src/views/app.rs +++ b/crates/app/src/chat_space.rs @@ -1,21 +1,23 @@ +use account::Account; use global::get_client; use gpui::{ actions, div, img, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis, Context, Entity, InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, Styled, - StyledImage, Window, + StyledImage, Subscription, Task, Window, }; use serde::Deserialize; +use smallvec::{smallvec, SmallVec}; use std::sync::Arc; use ui::{ button::{Button, ButtonRounded, ButtonVariants}, - dock_area::{dock::DockPlacement, DockArea, DockItem}, + dock_area::{dock::DockPlacement, panel::PanelView, DockArea, DockItem}, popup_menu::PopupMenuExt, theme::{scale::ColorScaleStep, ActiveTheme, Appearance, Theme}, ContextModal, Icon, IconName, Root, Sizable, TitleBar, }; -use super::{chat, contacts, onboarding, profile, relays, settings, sidebar, welcome}; -use crate::device::Device; +use crate::views::{chat, contacts, profile, relays, settings, welcome}; +use crate::views::{onboarding, sidebar}; #[derive(Clone, PartialEq, Eq, Deserialize)] pub enum PanelKind { @@ -43,25 +45,80 @@ impl_internal_actions!(dock, [AddPanel]); // Account actions actions!(account, [Logout]); -pub fn init(window: &mut Window, cx: &mut App) -> Entity { - AppView::new(window, cx) +pub fn init(window: &mut Window, cx: &mut App) -> Entity { + ChatSpace::new(window, cx) } -pub struct AppView { +pub struct ChatSpace { + titlebar: bool, dock: Entity, + #[allow(unused)] + subscriptions: SmallVec<[Subscription; 1]>, } -impl AppView { +impl ChatSpace { pub fn new(window: &mut Window, cx: &mut App) -> Entity { - // Initialize dock layout + let account = Account::global(cx); let dock = cx.new(|cx| DockArea::new(window, cx)); - let weak_dock = dock.downgrade(); + let titlebar = false; - // Initialize left dock - let left_panel = DockItem::panel(Arc::new(sidebar::init(window, cx))); + cx.new(|cx| { + let mut this = Self { + dock, + titlebar, + subscriptions: smallvec![cx.observe_in( + &account, + window, + |this: &mut ChatSpace, account, window, cx| { + if account.read(cx).profile.is_some() { + this.open_chats(window, cx); + } else { + this.open_onboarding(window, cx); + } + }, + )], + }; - // Initial central dock - let center_panel = DockItem::split_with_sizes( + if Account::global(cx).read(cx).profile.is_some() { + this.open_chats(window, cx); + } else { + this.open_onboarding(window, cx); + } + + this + }) + } + + pub fn set_center_panel(panel: P, window: &mut Window, cx: &mut App) { + 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 open_onboarding(&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.set_center(center, window, cx); + }); + } + + fn open_chats(&mut self, window: &mut Window, cx: &mut Context) { + self.show_titlebar(cx); + + let weak_dock = self.dock.downgrade(); + let left = DockItem::panel(Arc::new(sidebar::init(window, cx))); + let center = DockItem::split_with_sizes( Axis::Vertical, vec![DockItem::tabs( vec![Arc::new(welcome::init(window, cx))], @@ -76,16 +133,18 @@ impl AppView { cx, ); - // Set default dock layout with left and central docks - _ = weak_dock.update(cx, |view, cx| { - view.set_left_dock(left_panel, Some(px(240.)), true, window, cx); - view.set_center(center_panel, window, cx); + self.dock.update(cx, |this, cx| { + this.set_left_dock(left, Some(px(240.)), true, window, cx); + this.set_center(center, window, cx); }); - - cx.new(|_| Self { dock }) } - fn render_mode_btn(&self, cx: &mut Context) -> impl IntoElement { + fn show_titlebar(&mut self, cx: &mut Context) { + self.titlebar = true; + cx.notify(); + } + + fn render_appearance_btn(&self, cx: &mut Context) -> impl IntoElement { Button::new("appearance") .xsmall() .ghost() @@ -111,16 +170,17 @@ impl AppView { .xsmall() .reverse() .icon(Icon::new(IconName::ChevronDownSmall)) - .when_some(Device::global(cx), |this, account| { - this.when_some(account.read(cx).profile(), |this, profile| { + .when_some( + Account::global(cx).read(cx).profile.as_ref(), + |this, profile| { this.child( img(profile.avatar.clone()) .size_5() .rounded_full() .object_fit(ObjectFit::Cover), ) - }) - }) + }, + ) .popup_menu(move |this, _, _cx| { this.menu( "Profile", @@ -218,21 +278,27 @@ impl AppView { fn on_logout_action(&mut self, _action: &Logout, window: &mut Window, cx: &mut Context) { let client = get_client(); + let reset: Task> = cx.background_spawn(async move { + client.reset().await; + Ok(()) + }); - cx.background_spawn(async move { - // Reset nostr client - client.reset().await + cx.spawn_in(window, |_, mut cx| async move { + if reset.await.is_ok() { + cx.update(|_, cx| { + Account::global(cx).update(cx, |this, cx| { + this.profile = None; + cx.notify(); + }); + }) + .ok(); + }; }) .detach(); - - Root::update(window, cx, |this, window, cx| { - this.replace_view(onboarding::init(window, cx).into()); - cx.notify(); - }); } } -impl Render for AppView { +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); @@ -246,23 +312,25 @@ impl Render for AppView { .flex_col() .size_full() // Title Bar - .child( - TitleBar::new() - // Left side - .child(div()) - // Right side - .child( - div() - .flex() - .items_center() - .justify_end() - .gap_2() - .px_2() - .child(self.render_mode_btn(cx)) - .child(self.render_relays_btn(cx)) - .child(self.render_account_btn(cx)), - ), - ) + .when(self.titlebar, |this| { + this.child( + TitleBar::new() + // Left side + .child(div()) + // Right side + .child( + div() + .flex() + .items_center() + .justify_end() + .gap_2() + .px_2() + .child(self.render_appearance_btn(cx)) + .child(self.render_relays_btn(cx)) + .child(self.render_account_btn(cx)), + ), + ) + }) // Dock .child(self.dock.clone()), ) diff --git a/crates/app/src/device.rs b/crates/app/src/device.rs deleted file mode 100644 index 4d4a5c7..0000000 --- a/crates/app/src/device.rs +++ /dev/null @@ -1,916 +0,0 @@ -use std::{collections::HashSet, str::FromStr, sync::Arc, time::Duration}; - -use anyhow::{anyhow, Error}; -use common::profile::NostrProfile; -use global::{ - constants::{ - ALL_MESSAGES_SUB_ID, CLIENT_KEYRING, DEVICE_ANNOUNCEMENT_KIND, DEVICE_REQUEST_KIND, - DEVICE_RESPONSE_KIND, DEVICE_SUB_ID, MASTER_KEYRING, NEW_MESSAGE_SUB_ID, - }, - get_app_name, get_client, get_device_keys, get_device_name, set_device_keys, -}; -use gpui::{ - div, px, relative, App, AppContext, Context, Entity, Global, ParentElement, Styled, Task, - Window, -}; -use nostr_sdk::prelude::*; -use smallvec::SmallVec; -use ui::{ - button::{Button, ButtonRounded, ButtonVariants}, - indicator::Indicator, - notification::Notification, - theme::{scale::ColorScaleStep, ActiveTheme}, - ContextModal, Root, Sizable, StyledExt, -}; - -use crate::views::{app, onboarding, relays}; - -struct GlobalDevice(Entity); - -impl Global for GlobalDevice {} - -#[derive(Debug, Default)] -pub enum DeviceState { - Master, - Minion, - #[default] - None, -} - -impl DeviceState { - pub fn subscribe(&self, window: &mut Window, cx: &mut Context) { - match self { - Self::Master => { - let client = get_client(); - let task: Task> = cx.background_spawn(async move { - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - - let opts = - SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); - - let filter = Filter::new() - .kind(Kind::Custom(DEVICE_REQUEST_KIND)) - .author(public_key) - .limit(1); - - // Subscribe for the latest request - client.subscribe(filter, Some(opts)).await?; - - let filter = Filter::new() - .kind(Kind::Custom(DEVICE_REQUEST_KIND)) - .author(public_key) - .since(Timestamp::now()); - - // Subscribe for new device requests - client.subscribe(filter, None).await?; - - Ok(()) - }); - - cx.spawn_in(window, |_, _cx| async move { - if let Err(err) = task.await { - log::error!("Failed to subscribe for device requests: {}", err); - } - }) - .detach(); - } - Self::Minion => { - let client = get_client(); - let task: Task> = cx.background_spawn(async move { - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - - let opts = - SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); - - let filter = Filter::new() - .kind(Kind::Custom(DEVICE_RESPONSE_KIND)) - .author(public_key); - - // Getting all previous approvals - client.subscribe(filter.clone(), Some(opts)).await?; - - // Continously receive the request approval - client - .subscribe(filter.since(Timestamp::now()), None) - .await?; - - Ok(()) - }); - - cx.spawn_in(window, |_, _cx| async move { - if let Err(err) = task.await { - log::error!("Failed to subscribe for device approval: {}", err); - } - }) - .detach(); - } - _ => {} - } - } -} - -/// Current Device (Client) -/// -/// NIP-4e: -#[derive(Debug)] -pub struct Device { - /// Profile (Metadata) of current user - profile: Option, - /// Client Keys - client_keys: Arc, - /// Device State - state: Entity, - requesters: Entity>, - is_processing: bool, -} - -pub fn init(window: &mut Window, cx: &App) { - // Initialize client keys - let read_keys = cx.read_credentials(CLIENT_KEYRING); - let window_handle = window.window_handle(); - - cx.spawn(|cx| async move { - let client_keys = if let Ok(Some((_, secret))) = read_keys.await { - let secret_key = SecretKey::from_slice(&secret).unwrap(); - - Arc::new(Keys::new(secret_key)) - } else { - // Generate new keys and save them to keyring - let keys = Keys::generate(); - - if let Ok(write_keys) = cx.update(|cx| { - cx.write_credentials( - CLIENT_KEYRING, - keys.public_key.to_hex().as_str(), - keys.secret_key().as_secret_bytes(), - ) - }) { - _ = write_keys.await; - }; - - Arc::new(keys) - }; - - cx.update(|cx| { - let state = cx.new(|_| DeviceState::None); - let weak_state = state.downgrade(); - let requesters = cx.new(|_| HashSet::new()); - let entity = cx.new(|_| Device { - profile: None, - is_processing: false, - state, - client_keys, - requesters, - }); - - window_handle - .update(cx, |_, window, cx| { - // Open the onboarding view - Root::update(window, cx, |this, window, cx| { - this.replace_view(onboarding::init(window, cx).into()); - cx.notify(); - }); - - // Observe the DeviceState changes - if let Some(state) = weak_state.upgrade() { - window - .observe(&state, cx, |this, window, cx| { - this.update(cx, |this, cx| { - this.subscribe(window, cx); - }); - }) - .detach(); - }; - - // Observe the Device changes - window - .observe(&entity, cx, |this, window, cx| { - this.update(cx, |this, cx| { - this.on_device_change(window, cx); - }); - }) - .detach(); - }) - .ok(); - - Device::set_global(entity, cx) - }) - .ok(); - }) - .detach(); -} - -impl Device { - pub fn global(cx: &App) -> Option> { - cx.try_global::().map(|model| model.0.clone()) - } - - pub fn set_global(device: Entity, cx: &mut App) { - cx.set_global(GlobalDevice(device)); - } - - pub fn client_keys(&self) -> Arc { - self.client_keys.clone() - } - - pub fn profile(&self) -> Option<&NostrProfile> { - self.profile.as_ref() - } - - pub fn set_profile(&mut self, profile: NostrProfile, cx: &mut Context) { - self.profile = Some(profile); - cx.notify(); - } - - pub fn set_state(&mut self, state: DeviceState, cx: &mut Context) { - self.state.update(cx, |this, cx| { - *this = state; - cx.notify(); - }); - } - - pub fn set_processing(&mut self, is_processing: bool, cx: &mut Context) { - self.is_processing = is_processing; - cx.notify(); - } - - pub fn add_requester(&mut self, public_key: PublicKey, cx: &mut Context) { - self.requesters.update(cx, |this, cx| { - this.insert(public_key); - cx.notify(); - }); - } - - /// Login and set user signer - pub fn login(&self, signer: T, cx: &mut Context) -> Task> - where - T: NostrSigner + 'static, - { - let client = get_client(); - - // Set the user's signer as the main signer - let login: Task> = cx.background_spawn(async move { - // Use user's signer for main signer - _ = client.set_signer(signer).await; - - // Verify nostr signer and get public key - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - - // Fetch user's metadata - let metadata = client - .fetch_metadata(public_key, Duration::from_secs(2)) - .await? - .unwrap_or_default(); - - // Get user's inbox relays - let filter = Filter::new() - .kind(Kind::InboxRelays) - .author(public_key) - .limit(1); - - let relays = if let Some(event) = client - .fetch_events(filter, Duration::from_secs(2)) - .await? - .first_owned() - { - let relays = event - .tags - .filter_standardized(TagKind::Relay) - .filter_map(|t| { - if let TagStandard::Relay(url) = t { - Some(url.to_owned()) - } else { - None - } - }) - .collect::>(); - - Some(relays) - } else { - None - }; - - let profile = NostrProfile::new(public_key, metadata).relays(relays); - - Ok(profile) - }); - - cx.spawn(|this, cx| async move { - match login.await { - Ok(user) => { - cx.update(|cx| { - this.update(cx, |this, cx| { - this.profile = Some(user); - cx.notify(); - }) - .ok(); - }) - .ok(); - - Ok(()) - } - Err(e) => Err(e), - } - }) - } - - /// This function is called whenever the device is changed - fn on_device_change(&mut self, window: &mut Window, cx: &mut Context) { - let Some(profile) = self.profile.as_ref() else { - // User not logged in, render the Onboarding View - Root::update(window, cx, |this, window, cx| { - this.replace_view(onboarding::init(window, cx).into()); - cx.notify(); - }); - - return; - }; - - // Replace the Onboarding View with the Dock View - Root::update(window, cx, |this, window, cx| { - this.replace_view(app::init(window, cx).into()); - cx.notify(); - }); - - // Get the user's messaging relays - // If it is empty, user must setup relays - let ready = profile.messaging_relays.is_some(); - - cx.spawn_in(window, |this, mut cx| async move { - cx.update(|window, cx| { - if !ready { - this.update(cx, |this, cx| { - this.render_setup_relays(window, cx); - }) - .ok(); - } else { - this.update(cx, |this, cx| { - this.start_subscription(cx); - }) - .ok(); - } - }) - .ok(); - }) - .detach(); - } - - /// Initialize subscription for current user - pub fn start_subscription(&self, cx: &Context) { - if self.is_processing { - return; - } - - let Some(profile) = self.profile() else { - return; - }; - - let user = profile.public_key; - let client = get_client(); - - let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE); - let device_kind = Kind::Custom(DEVICE_ANNOUNCEMENT_KIND); - - // Create a device announcement filter - let device = Filter::new().kind(device_kind).author(user).limit(1); - - // Create a contact list filter - let contacts = Filter::new().kind(Kind::ContactList).author(user).limit(1); - - // Create a user's data filter - let data = Filter::new() - .author(user) - .since(Timestamp::now()) - .kinds(vec![ - Kind::Metadata, - Kind::InboxRelays, - Kind::RelayList, - device_kind, - ]); - - // Create a filter for getting all gift wrapped events send to current user - let msg = Filter::new().kind(Kind::GiftWrap).pubkey(user); - - // Create a filter to continuously receive new messages. - let new_msg = Filter::new().kind(Kind::GiftWrap).pubkey(user).limit(0); - - let task: Task> = cx.background_spawn(async move { - // Only subscribe to the latest device announcement - let sub_id = SubscriptionId::new(DEVICE_SUB_ID); - client.subscribe_with_id(sub_id, device, Some(opts)).await?; - - // Only subscribe to the latest contact list - client.subscribe(contacts, Some(opts)).await?; - - // Continuously receive new user's data since now - client.subscribe(data, None).await?; - - let sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID); - client.subscribe_with_id(sub_id, msg, Some(opts)).await?; - - let sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID); - client.subscribe_with_id(sub_id, new_msg, None).await?; - - Ok(()) - }); - - cx.spawn(|_, _| async move { - if let Err(e) = task.await { - log::error!("Subscription error: {}", e); - } - }) - .detach(); - } - - /// Setup Device - /// - /// NIP-4e: - pub fn setup_device(&mut self, window: &mut Window, cx: &mut Context) { - let Some(profile) = self.profile().cloned() else { - return; - }; - - // If processing, return early - if self.is_processing { - return; - } - - // Only process if device keys are not set - self.set_processing(true, cx); - - let client = get_client(); - let public_key = profile.public_key; - let kind = Kind::Custom(DEVICE_ANNOUNCEMENT_KIND); - let filter = Filter::new().kind(kind).author(public_key).limit(1); - - // Fetch device announcement events - let fetch_announcement = cx.background_spawn(async move { - if let Some(event) = client.database().query(filter).await?.first_owned() { - Ok(event) - } else { - Err(anyhow!("Device Announcement not found.")) - } - }); - - cx.spawn_in(window, |this, mut cx| async move { - // Device Keys has been set, no need to retrieve device announcement again - if get_device_keys().await.is_some() { - return; - } - - match fetch_announcement.await { - Ok(event) => { - log::info!("Found a device announcement: {:?}", event); - - let n_tag = event - .tags - .find(TagKind::custom("n")) - .and_then(|t| t.content()) - .map(|hex| hex.to_owned()); - - let credentials_task = - match cx.update(|_, cx| cx.read_credentials(MASTER_KEYRING)) { - Ok(task) => task, - Err(err) => { - log::error!("Failed to read credentials: {:?}", err); - log::info!("Trying to request keys from Master Device..."); - - cx.update(|window, cx| { - this.update(cx, |this, cx| { - this.request_master_keys(window, cx); - }) - }) - .ok(); - - return; - } - }; - - match credentials_task.await { - Ok(Some((pubkey, secret))) if n_tag.as_deref() == Some(&pubkey) => { - cx.update(|window, cx| { - this.update(cx, |this, cx| { - this.set_master_keys(secret, window, cx); - }) - }) - .ok(); - } - _ => { - log::info!("This device is not the Master Device."); - log::info!("Trying to request keys from Master Device..."); - - cx.update(|window, cx| { - this.update(cx, |this, cx| { - this.request_master_keys(window, cx); - }) - }) - .ok(); - } - } - } - Err(_) => { - log::info!("Device Announcement not found."); - log::info!("Appoint this device as Master Device."); - - cx.update(|window, cx| { - this.update(cx, |this, cx| { - this.set_new_master_keys(window, cx); - }) - .ok(); - }) - .ok(); - } - } - }) - .detach(); - } - - /// Create a new Master Keys, appointing this device as Master Device. - /// - /// NIP-4e: - pub fn set_new_master_keys(&self, window: &mut Window, cx: &Context) { - let client = get_client(); - let app_name = get_app_name(); - - let task: Task, Error>> = cx.background_spawn(async move { - let keys = Keys::generate(); - let kind = Kind::Custom(DEVICE_ANNOUNCEMENT_KIND); - let client_tag = Tag::client(app_name); - let pubkey_tag = Tag::custom(TagKind::custom("n"), vec![keys.public_key().to_hex()]); - - let event = EventBuilder::new(kind, "").tags(vec![client_tag, pubkey_tag]); - - if let Err(e) = client.send_event_builder(event).await { - log::error!("Failed to send Device Announcement: {}", e); - } else { - log::info!("Device Announcement has been sent"); - } - - Ok(Arc::new(keys)) - }); - - cx.spawn_in(window, |this, mut cx| async move { - if get_device_keys().await.is_some() { - return; - } - - if let Ok(keys) = task.await { - // Update global state - set_device_keys(keys.clone()).await; - - // Save keys - if let Ok(task) = cx.update(|_, cx| { - cx.write_credentials( - MASTER_KEYRING, - keys.public_key().to_hex().as_str(), - keys.secret_key().as_secret_bytes(), - ) - }) { - if let Err(e) = task.await { - log::error!("Failed to write device keys to keyring: {}", e); - } - }; - - cx.update(|_, cx| { - this.update(cx, |this, cx| { - this.set_state(DeviceState::Master, cx); - }) - .ok(); - }) - .ok(); - } - }) - .detach(); - } - - /// Device already has Master Keys, re-appointing this device as Master Device. - /// - /// NIP-4e: - pub fn set_master_keys(&self, secret: Vec, window: &mut Window, cx: &Context) { - let Ok(secret_key) = SecretKey::from_slice(&secret) else { - log::error!("Failed to parse secret key"); - return; - }; - let keys = Arc::new(Keys::new(secret_key)); - - cx.spawn_in(window, |this, mut cx| async move { - log::info!("Re-appointing this device as Master Device."); - set_device_keys(keys).await; - - cx.update(|_, cx| { - this.update(cx, |this, cx| { - this.set_state(DeviceState::Master, cx); - }) - .ok(); - }) - .ok(); - }) - .detach(); - } - - /// Send a request to ask for device keys from the other Nostr client - /// - /// NIP-4e: - pub fn request_master_keys(&self, window: &mut Window, cx: &Context) { - let client = get_client(); - let app_name = get_app_name(); - let client_keys = self.client_keys.clone(); - - let kind = Kind::Custom(DEVICE_REQUEST_KIND); - let client_tag = Tag::client(app_name); - let pubkey_tag = Tag::custom( - TagKind::custom("pubkey"), - vec![client_keys.public_key().to_hex()], - ); - - // Create a request event builder - let builder = EventBuilder::new(kind, "").tags(vec![client_tag, pubkey_tag]); - - let task: Task> = cx.background_spawn(async move { - log::info!("Sent a request to ask for device keys from the other Nostr client"); - - if let Err(e) = client.send_event_builder(builder).await { - log::error!("Failed to send device keys request: {}", e); - } else { - log::info!("Waiting for response..."); - } - - Ok(()) - }); - - cx.spawn_in(window, |this, mut cx| async move { - if task.await.is_ok() { - cx.update(|window, cx| { - this.update(cx, |this, cx| { - this.set_state(DeviceState::Minion, cx); - this.render_waiting_modal(window, cx); - }) - .ok(); - }) - .ok(); - } - }) - .detach(); - } - - /// Received Device Keys approval from Master Device, - /// - /// NIP-4e: - pub fn recv_approval(&self, event: Event, window: &mut Window, cx: &Context) { - let local_signer = self.client_keys.clone(); - - let task = cx.background_spawn(async move { - if let Some(tag) = event - .tags - .find(TagKind::custom("P")) - .and_then(|tag| tag.content()) - { - if let Ok(public_key) = PublicKey::from_str(tag) { - let secret = local_signer - .nip44_decrypt(&public_key, &event.content) - .await?; - - let keys = Arc::new(Keys::parse(&secret)?); - - // Update global state with new device keys - set_device_keys(keys).await; - log::info!("Received master keys"); - - Ok(()) - } else { - Err(anyhow!("Public Key is invalid")) - } - } else { - Err(anyhow!("Failed to decrypt the Master Keys")) - } - }); - - cx.spawn_in(window, |_, mut cx| async move { - // No need to update if device keys are already available - if get_device_keys().await.is_some() { - return; - } - - if let Err(e) = task.await { - cx.update(|window, cx| { - window.push_notification( - Notification::error(format!("Failed to decrypt: {}", e)), - cx, - ); - }) - .ok(); - } else { - cx.update(|window, cx| { - window.close_all_modals(cx); - window.push_notification( - Notification::success("Device Keys request has been approved"), - cx, - ); - }) - .ok(); - } - }) - .detach(); - } - - /// Received Master Keys request from other Nostr client - /// - /// NIP-4e: - pub fn recv_request(&mut self, event: Event, window: &mut Window, cx: &mut Context) { - let Some(target_pubkey) = event - .tags - .find(TagKind::custom("pubkey")) - .and_then(|tag| tag.content()) - .and_then(|content| PublicKey::parse(content).ok()) - else { - log::error!("Invalid public key."); - return; - }; - - // Prevent processing duplicate requests - if self.requesters.read(cx).contains(&target_pubkey) { - return; - } - - self.add_requester(target_pubkey, cx); - - let client = get_client(); - let read_keys = cx.read_credentials(MASTER_KEYRING); - let local_signer = self.client_keys.clone(); - - let device_name = event - .tags - .find(TagKind::Client) - .and_then(|tag| tag.content()) - .unwrap_or("Other Device") - .to_owned(); - - let response = window.prompt( - gpui::PromptLevel::Info, - "Requesting Device Keys", - Some( - format!( - "{} is requesting shared device keys stored in this device", - device_name - ) - .as_str(), - ), - &["Approve", "Deny"], - cx, - ); - - cx.spawn_in(window, |_, cx| async move { - match response.await { - Ok(0) => { - if let Ok(Some((_, secret))) = read_keys.await { - let local_pubkey = local_signer.get_public_key().await?; - - // Get device's secret key - let device_secret = SecretKey::from_slice(&secret)?; - let device_secret_hex = device_secret.to_secret_hex(); - - // Encrypt device's secret key by using NIP-44 - let content = local_signer - .nip44_encrypt(&target_pubkey, &device_secret_hex) - .await?; - - // Create pubkey tag for other device (lowercase p) - let other_tag = Tag::public_key(target_pubkey); - - // Create pubkey tag for this device (uppercase P) - let local_tag = Tag::custom( - TagKind::SingleLetter(SingleLetterTag::uppercase(Alphabet::P)), - vec![local_pubkey.to_hex()], - ); - - // Create event builder - let kind = Kind::Custom(DEVICE_RESPONSE_KIND); - let tags = vec![other_tag, local_tag]; - let builder = EventBuilder::new(kind, content).tags(tags); - - cx.background_spawn(async move { - if let Err(err) = client.send_event_builder(builder).await { - log::error!("Failed to send device keys to other client: {}", err); - } else { - log::info!("Sent device keys to other client"); - } - }) - .await; - - Ok(()) - } else { - Err(anyhow!("Device Keys not found")) - } - } - _ => Ok(()), - } - }) - .detach(); - } - - /// Show setup relays modal - /// - /// NIP-17: - pub fn render_setup_relays(&mut self, window: &mut Window, cx: &mut Context) { - let relays = relays::init(window, cx); - - window.open_modal(cx, move |this, window, cx| { - let is_loading = relays.read(cx).loading(); - - this.keyboard(false) - .closable(false) - .width(px(430.)) - .title("Your Messaging Relays are not configured") - .child(relays.clone()) - .footer( - div() - .p_2() - .border_t_1() - .border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE)) - .child( - Button::new("update_inbox_relays_btn") - .label("Update") - .primary() - .bold() - .rounded(ButtonRounded::Large) - .w_full() - .loading(is_loading) - .on_click(window.listener_for(&relays, |this, _, window, cx| { - this.update(window, cx); - })), - ), - ) - }); - } - - /// Show waiting modal - /// - /// NIP-4e: - pub fn render_waiting_modal(&mut self, window: &mut Window, cx: &mut Context) { - window.open_modal(cx, move |this, _window, cx| { - let msg = format!( - "Please open {} and approve sharing device keys request.", - get_device_name() - ); - - this.keyboard(false) - .closable(false) - .width(px(430.)) - .child( - div() - .flex() - .items_center() - .justify_center() - .size_full() - .p_4() - .child( - div() - .flex() - .flex_col() - .items_center() - .justify_center() - .size_full() - .child( - div() - .flex() - .flex_col() - .text_sm() - .child( - div() - .font_semibold() - .child("You're using a new device."), - ) - .child( - div() - .text_color( - cx.theme() - .base - .step(cx, ColorScaleStep::ELEVEN), - ) - .line_height(relative(1.3)) - .child(msg), - ), - ), - ), - ) - .footer( - div() - .p_4() - .border_t_1() - .border_color(cx.theme().base.step(cx, ColorScaleStep::FIVE)) - .w_full() - .flex() - .gap_2() - .items_center() - .justify_center() - .text_xs() - .text_color(cx.theme().base.step(cx, ColorScaleStep::ELEVEN)) - .child(Indicator::new().small()) - .child("Waiting for approval ..."), - ) - }); - } -} diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index 3848134..7a0709f 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -1,16 +1,11 @@ -use anyhow::anyhow; use asset::Assets; -use chats::registry::ChatRegistry; -use device::Device; +use chats::ChatRegistry; use futures::{select, FutureExt}; #[cfg(not(target_os = "linux"))] use global::constants::APP_NAME; use global::{ - constants::{ - ALL_MESSAGES_SUB_ID, APP_ID, BOOTSTRAP_RELAYS, DEVICE_ANNOUNCEMENT_KIND, - DEVICE_REQUEST_KIND, DEVICE_RESPONSE_KIND, DEVICE_SUB_ID, NEW_MESSAGE_SUB_ID, - }, - get_client, get_device_keys, set_device_name, + constants::{ALL_MESSAGES_SUB_ID, APP_ID, BOOTSTRAP_RELAYS, NEW_MESSAGE_SUB_ID}, + get_client, }; use gpui::{ actions, px, size, App, AppContext, Application, Bounds, KeyBinding, Menu, MenuItem, @@ -21,16 +16,15 @@ use gpui::{point, SharedString, TitlebarOptions}; #[cfg(target_os = "linux")] use gpui::{WindowBackgroundAppearance, WindowDecorations}; use nostr_sdk::{ - nips::nip59::UnwrappedGift, pool::prelude::ReqExitPolicy, Event, Filter, Keys, Kind, PublicKey, - RelayMessage, RelayPoolNotification, SubscribeAutoCloseOptions, SubscriptionId, TagKind, + pool::prelude::ReqExitPolicy, Event, Filter, Keys, Kind, PublicKey, RelayMessage, + RelayPoolNotification, SubscribeAutoCloseOptions, SubscriptionId, }; use smol::Timer; use std::{collections::HashSet, mem, sync::Arc, time::Duration}; use ui::{theme::Theme, Root}; -use views::startup; pub(crate) mod asset; -pub(crate) mod device; +pub(crate) mod chat_space; pub(crate) mod views; actions!(coop, [Quit]); @@ -39,12 +33,6 @@ actions!(coop, [Quit]); enum Signal { /// Receive event Event(Event), - /// Receive request master key event - RequestMasterKey(Event), - /// Receive approve master key event - ReceiveMasterKey(Event), - /// Receive announcement event - ReceiveAnnouncement, /// Receive EOSE Eose, } @@ -121,7 +109,6 @@ fn main() { let rng_keys = Keys::generate(); let all_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID); let new_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID); - let device_id = SubscriptionId::new(DEVICE_SUB_ID); let mut notifications = client.notifications(); while let Ok(notification) = notifications.recv().await { @@ -133,7 +120,7 @@ fn main() { } => { match event.kind { Kind::GiftWrap => { - if let Ok(gift) = handle_gift_wrap(&event).await { + if let Ok(gift) = client.unwrap_gift_wrap(&event).await { // Sign the rumor with the generated keys, // this event will be used for internal only, // and NEVER send to relays. @@ -161,45 +148,12 @@ fn main() { handle_metadata(pubkeys).await; } - Kind::Custom(DEVICE_REQUEST_KIND) => { - log::info!("Received device keys request"); - - _ = event_tx - .send(Signal::RequestMasterKey(event.into_owned())) - .await; - } - Kind::Custom(DEVICE_RESPONSE_KIND) => { - log::info!("Received device keys approval"); - - _ = event_tx - .send(Signal::ReceiveMasterKey(event.into_owned())) - .await; - } - Kind::Custom(DEVICE_ANNOUNCEMENT_KIND) => { - log::info!("Device Announcement received"); - - if let Ok(signer) = client.signer().await { - if let Ok(public_key) = signer.get_public_key().await { - if event.pubkey == public_key { - if let Some(tag) = event - .tags - .find(TagKind::custom("client")) - .and_then(|tag| tag.content()) - { - set_device_name(tag).await; - } - } - } - } - } _ => {} } } RelayMessage::EndOfStoredEvents(subscription_id) => { if all_id == *subscription_id { _ = event_tx.send(Signal::Eose).await; - } else if device_id == *subscription_id { - _ = event_tx.send(Signal::ReceiveAnnouncement).await; } } _ => {} @@ -256,53 +210,26 @@ fn main() { }) .detach(); - // Initialize components - ui::init(cx); - - // Initialize chat global state - chats::registry::init(cx); - - // Initialize device - device::init(window, cx); - + // Root Entity cx.new(|cx| { - let root = Root::new(startup::init(window, cx).into(), window, cx); - + // Initialize components + ui::init(cx); + // Initialize chat state + chats::init(cx); + // Initialize account state + account::init(cx); // Spawn a task to handle events from nostr channel cx.spawn_in(window, |_, mut cx| async move { + let chats = cx.update(|_, cx| ChatRegistry::global(cx)).unwrap(); + while let Ok(signal) = event_rx.recv().await { - cx.update(|window, cx| { + cx.update(|_, cx| { match signal { Signal::Eose => { - if let Some(chats) = ChatRegistry::global(cx) { - chats.update(cx, |this, cx| this.load_chat_rooms(cx)) - } + chats.update(cx, |this, cx| this.load_chat_rooms(cx)); } Signal::Event(event) => { - if let Some(chats) = ChatRegistry::global(cx) { - chats.update(cx, |this, cx| this.push_message(event, cx)) - } - } - Signal::ReceiveAnnouncement => { - if let Some(device) = Device::global(cx) { - device.update(cx, |this, cx| { - this.setup_device(window, cx); - }); - } - } - Signal::ReceiveMasterKey(event) => { - if let Some(device) = Device::global(cx) { - device.update(cx, |this, cx| { - this.recv_approval(event, window, cx); - }); - } - } - Signal::RequestMasterKey(event) => { - if let Some(device) = Device::global(cx) { - device.update(cx, |this, cx| { - this.recv_request(event, window, cx); - }); - } + chats.update(cx, |this, cx| this.push_message(event, cx)); } }; }) @@ -311,43 +238,24 @@ fn main() { }) .detach(); - root + Root::new(chat_space::init(window, cx).into(), window, cx) }) }) .expect("Failed to open window. Please restart the application."); }); } -async fn handle_gift_wrap(gift_wrap: &Event) -> Result { - let client = get_client(); - - if let Some(device) = get_device_keys().await { - // Try to unwrap with the device keys first - match UnwrappedGift::from_gift_wrap(&device, gift_wrap).await { - Ok(event) => Ok(event), - Err(_) => { - // Try to unwrap again with the user's signer - let signer = client.signer().await?; - let event = UnwrappedGift::from_gift_wrap(&signer, gift_wrap).await?; - Ok(event) - } - } - } else { - Err(anyhow!("Signer not found")) - } -} - async fn handle_metadata(buffer: HashSet) { let client = get_client(); let opts = SubscribeAutoCloseOptions::default() .exit_policy(ReqExitPolicy::ExitOnEOSE) - .idle_timeout(Some(Duration::from_secs(2))); + .idle_timeout(Some(Duration::from_secs(1))); let filter = Filter::new() .authors(buffer.iter().cloned()) - .limit(buffer.len() * 2) - .kinds(vec![Kind::Metadata, Kind::Custom(DEVICE_ANNOUNCEMENT_KIND)]); + .limit(100) + .kinds(vec![Kind::Metadata, Kind::UserStatus]); if let Err(e) = client.subscribe(filter, Some(opts)).await { log::error!("Failed to sync metadata: {e}"); diff --git a/crates/app/src/views/chat.rs b/crates/app/src/views/chat.rs index ca3d649..2b03dd4 100644 --- a/crates/app/src/views/chat.rs +++ b/crates/app/src/views/chat.rs @@ -1,6 +1,6 @@ use anyhow::anyhow; use async_utility::task::spawn; -use chats::{registry::ChatRegistry, room::Room}; +use chats::{room::Room, ChatRegistry}; use common::{ last_seen::LastSeen, profile::NostrProfile, @@ -37,14 +37,10 @@ pub fn init( window: &mut Window, cx: &mut App, ) -> Result>, anyhow::Error> { - if let Some(chats) = ChatRegistry::global(cx) { - if let Some(room) = chats.read(cx).get(id, cx) { - Ok(Arc::new(Chat::new(id, room, window, cx))) - } else { - Err(anyhow!("Chat room is not exist")) - } + if let Some(room) = ChatRegistry::global(cx).read(cx).get(id, cx) { + Ok(Arc::new(Chat::new(id, room, window, cx))) } else { - Err(anyhow!("Chat Registry is not initialized")) + Err(anyhow!("Chat room is not exist")) } } diff --git a/crates/app/src/views/login.rs b/crates/app/src/views/login.rs new file mode 100644 index 0000000..6bed937 --- /dev/null +++ b/crates/app/src/views/login.rs @@ -0,0 +1,433 @@ +use std::time::Duration; + +use account::Account; +use common::utils::create_qr; +use global::get_client_keys; +use gpui::{ + div, img, prelude::FluentBuilder, 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 ui::{ + button::{Button, ButtonVariants}, + dock_area::panel::{Panel, PanelEvent}, + input::{InputEvent, TextInput}, + notification::Notification, + popup_menu::PopupMenu, + theme::{scale::ColorScaleStep, ActiveTheme}, + ContextModal, Disableable, Icon, IconName, Sizable, Size, StyledExt, +}; + +use crate::chat_space::ChatSpace; + +use super::onboarding; + +const INPUT_INVALID: &str = "You must provide a valid Private Key or Bunker."; + +pub fn init(window: &mut Window, cx: &mut App) -> Entity { + Login::new(window, cx) +} + +pub struct Login { + // Inputs + key_input: Entity, + error_message: Entity>, + is_logging_in: bool, + // Nostr Connect + connect_relay: Entity, + connect_client: Entity>, + // Panel + name: SharedString, + closable: bool, + zoomable: bool, + focus_handle: FocusHandle, + #[allow(unused)] + subscriptions: SmallVec<[Subscription; 3]>, +} + +impl Login { + 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 mut subscriptions = smallvec![]; + let error_message = cx.new(|_| None); + let connect_client = cx.new(|_: &mut Context<'_, Option>| None); + + let key_input = cx.new(|cx| { + TextInput::new(window, cx) + .text_size(Size::XSmall) + .placeholder("nsec... or bunker://...") + }); + + let connect_relay = cx.new(|cx| { + let mut input = TextInput::new(window, cx).text_size(Size::XSmall).small(); + input.set_text("wss://relay.nsec.app", window, cx); + input + }); + + subscriptions.push(cx.subscribe_in( + &key_input, + window, + move |this, _, event, window, cx| { + if let InputEvent::PressEnter = event { + this.login(window, cx); + } + }, + )); + + subscriptions.push(cx.subscribe_in( + &connect_relay, + window, + move |this, _, event, window, cx| { + if let InputEvent::PressEnter = event { + this.change_relay(window, cx); + } + }, + )); + + subscriptions.push( + cx.observe_in(&connect_client, window, |_, this, window, cx| { + let keys = get_client_keys().to_owned(); + let account = Account::global(cx); + + if let Some(uri) = this.read(cx) { + match NostrConnect::new(uri.to_owned(), keys, Duration::from_secs(300), None) { + Ok(signer) => { + account.update(cx, |this, cx| { + this.login(signer, window, cx); + }); + } + Err(e) => { + window.push_notification(Notification::error(e.to_string()), cx); + } + } + } + }), + ); + + cx.spawn(|this, cx| async move { + cx.background_executor() + .timer(Duration::from_millis(500)) + .await; + + cx.update(|cx| { + this.update(cx, |this, cx| { + let Ok(relay_url) = + RelayUrl::parse(this.connect_relay.read(cx).text().to_string().as_str()) + else { + return; + }; + + let client_pubkey = get_client_keys().public_key(); + let uri = NostrConnectURI::client(client_pubkey, vec![relay_url], "Coop"); + + this.connect_client.update(cx, |this, cx| { + *this = Some(uri); + cx.notify(); + }); + }) + }) + .ok(); + }) + .detach(); + + Self { + key_input, + connect_relay, + connect_client, + subscriptions, + error_message, + is_logging_in: false, + name: "Login".into(), + closable: true, + zoomable: true, + focus_handle: cx.focus_handle(), + } + } + + fn login(&mut self, window: &mut Window, cx: &mut Context) { + self.set_logging_in(true, cx); + + let content = self.key_input.read(cx).text(); + let account = Account::global(cx); + + if content.starts_with("nsec1") { + match SecretKey::parse(content.as_ref()) { + Ok(secret) => { + let keys = Keys::new(secret); + + account.update(cx, |this, cx| { + this.login(keys, window, cx); + }); + } + Err(e) => { + self.set_error_message(e.to_string(), cx); + self.set_logging_in(false, cx); + } + } + } else if content.starts_with("bunker://") { + let keys = get_client_keys().to_owned(); + let Ok(uri) = NostrConnectURI::parse(content.as_ref()) else { + self.set_error_message("Bunker URL is not valid".to_owned(), cx); + self.set_logging_in(false, cx); + return; + }; + + match NostrConnect::new(uri, keys, Duration::from_secs(120), None) { + Ok(signer) => { + account.update(cx, |this, cx| { + this.login(signer, window, cx); + }); + } + Err(e) => { + self.set_error_message(e.to_string(), cx); + self.set_logging_in(false, cx); + } + } + } else { + self.set_logging_in(false, cx); + window.push_notification(Notification::error(INPUT_INVALID), cx); + }; + } + + fn change_relay(&mut self, window: &mut Window, cx: &mut Context) { + let Ok(relay_url) = + RelayUrl::parse(self.connect_relay.read(cx).text().to_string().as_str()) + else { + window.push_notification(Notification::error("Relay URL is not valid."), cx); + return; + }; + + let client_pubkey = get_client_keys().public_key(); + let uri = NostrConnectURI::client(client_pubkey, vec![relay_url], "Coop"); + + self.connect_client.update(cx, |this, cx| { + *this = Some(uri); + cx.notify(); + }); + } + + fn set_error_message(&mut self, message: String, cx: &mut Context) { + self.error_message.update(cx, |this, cx| { + *this = Some(SharedString::new(message)); + cx.notify(); + }); + } + + fn set_logging_in(&mut self, status: bool, cx: &mut Context) { + self.is_logging_in = status; + cx.notify(); + } + + fn back(&self, window: &mut Window, cx: &mut Context) { + let panel = onboarding::init(window, cx); + ChatSpace::set_center_panel(panel, window, cx); + } +} + +impl Panel for Login { + fn panel_id(&self) -> SharedString { + self.name.clone() + } + + fn title(&self, _cx: &App) -> AnyElement { + self.name.clone().into_any_element() + } + + fn closable(&self, _cx: &App) -> bool { + self.closable + } + + fn zoomable(&self, _cx: &App) -> bool { + self.zoomable + } + + fn popup_menu(&self, menu: PopupMenu, _cx: &App) -> PopupMenu { + menu.track_focus(&self.focus_handle) + } + + fn toolbar_buttons(&self, _window: &Window, _cx: &App) -> Vec