From c8315a2f939f240e797d27ef800aa04dc38101bd Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 4 Dec 2024 14:52:20 +0700 Subject: [PATCH] wip: refactor --- assets/icons/loader.svg | 10 ++ crates/app/Cargo.toml | 1 + crates/app/src/constants.rs | 2 + crates/app/src/main.rs | 133 ++++++++--------- crates/app/src/states/account.rs | 60 ++++++++ crates/app/src/states/mod.rs | 2 +- crates/app/src/states/room.rs | 98 +++++++++++-- crates/app/src/states/user.rs | 19 --- crates/app/src/ui/app.rs | 144 ++++++++----------- crates/app/src/ui/block/mod.rs | 1 + crates/app/src/ui/block/rooms.rs | 157 ++++++++++++++++++++ crates/app/src/ui/block/sidebar.rs | 160 --------------------- crates/app/src/ui/chat_space/bottom_bar.rs | 96 ------------- crates/app/src/ui/chat_space/mod.rs | 49 ------- crates/app/src/ui/chat_space/navigation.rs | 86 ----------- crates/app/src/ui/onboarding.rs | 60 ++++---- 16 files changed, 463 insertions(+), 615 deletions(-) create mode 100644 assets/icons/loader.svg create mode 100644 crates/app/src/states/account.rs delete mode 100644 crates/app/src/states/user.rs create mode 100644 crates/app/src/ui/block/rooms.rs delete mode 100644 crates/app/src/ui/block/sidebar.rs delete mode 100644 crates/app/src/ui/chat_space/bottom_bar.rs delete mode 100644 crates/app/src/ui/chat_space/mod.rs delete mode 100644 crates/app/src/ui/chat_space/navigation.rs diff --git a/assets/icons/loader.svg b/assets/icons/loader.svg new file mode 100644 index 0000000..f6ff93f --- /dev/null +++ b/assets/icons/loader.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 7370d9e..c747845 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -26,3 +26,4 @@ dirs.workspace = true tracing-subscriber = { version = "0.3.18", features = ["fmt"] } rust-embed = "8.5.0" +smol = "1" diff --git a/crates/app/src/constants.rs b/crates/app/src/constants.rs index 4096e93..625a0e5 100644 --- a/crates/app/src/constants.rs +++ b/crates/app/src/constants.rs @@ -1,3 +1,5 @@ pub const KEYRING_SERVICE: &str = "Coop Safe Storage"; pub const APP_NAME: &str = "coop"; pub const FAKE_SIG: &str = "f9e79d141c004977192d05a86f81ec7c585179c371f7350a5412d33575a2a356433f58e405c2296ed273e2fe0aafa25b641e39cc4e1f3f261ebf55bce0cbac83"; +pub const SUBSCRIPTION_ID: &str = "listen_new_giftwrap"; +pub const METADATA_DELAY: u64 = 150; diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index 4bc998d..8350104 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -1,13 +1,17 @@ use asset::Assets; use components::Root; -use constants::{APP_NAME, FAKE_SIG}; use dirs::config_dir; use gpui::*; use nostr_sdk::prelude::*; -use std::{fs, str::FromStr, sync::Arc, time::Duration}; -use tokio::sync::OnceCell; +use std::{ + fs, + str::FromStr, + sync::{Arc, OnceLock}, + time::Duration, +}; -use states::user::UserState; +use constants::{APP_NAME, FAKE_SIG}; +use states::account::AccountState; use ui::app::AppView; pub mod asset; @@ -18,77 +22,71 @@ pub mod utils; actions!(main_menu, [Quit]); -pub static CLIENT: OnceCell = OnceCell::const_new(); +static CLIENT: OnceLock = OnceLock::new(); -pub async fn get_client() -> &'static Client { - CLIENT - .get_or_init(|| async { - // Setup app data folder - let config_dir = config_dir().expect("Config directory not found"); - let _ = fs::create_dir_all(config_dir.join("Coop/")); +pub fn initialize_client() { + // Setup app data folder + let config_dir = config_dir().expect("Config directory not found"); + let _ = fs::create_dir_all(config_dir.join("Coop/")); - // Setup database - let lmdb = NostrLMDB::open(config_dir.join("Coop/nostr")) - .expect("Database is NOT initialized"); + // Setup database + let lmdb = NostrLMDB::open(config_dir.join("Coop/nostr")).expect("Database is NOT initialized"); - // Client options - let opts = Options::new() - .gossip(true) - .max_avg_latency(Duration::from_secs(2)); + // Client options + let opts = Options::new() + .gossip(true) + .max_avg_latency(Duration::from_secs(2)); - // Setup Nostr Client - let client = ClientBuilder::default().database(lmdb).opts(opts).build(); + // Setup Nostr Client + let client = ClientBuilder::default().database(lmdb).opts(opts).build(); - // Add some bootstrap relays - let _ = client.add_relay("wss://relay.damus.io").await; - let _ = client.add_relay("wss://relay.primal.net").await; + CLIENT.set(client).expect("Client is already initialized!"); +} - let _ = client.add_discovery_relay("wss://directory.yabu.me").await; - let _ = client.add_discovery_relay("wss://user.kindpag.es/").await; - - // Connect to all relays - client.connect().await; - - // Return client - client - }) - .await +pub fn get_client() -> &'static Client { + CLIENT.get().expect("Client is NOT initialized!") } #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); - // Initialize nostr client - let _client = get_client().await; + // Initialize client + initialize_client(); + + // Get client + let client = get_client(); + let mut notifications = client.notifications(); + + // Add some bootstrap relays + _ = client.add_relay("wss://relay.damus.io/").await; + _ = client.add_relay("wss://relay.primal.net/").await; + _ = client.add_relay("wss://nos.lol/").await; + + _ = client.add_discovery_relay("wss://user.kindpag.es/").await; + _ = client.add_discovery_relay("wss://directory.yabu.me/").await; + + // Connect to all relays + _ = client.connect().await; App::new() .with_assets(Assets) .with_http_client(Arc::new(reqwest_client::ReqwestClient::new())) .run(move |cx| { + AccountState::set_global(cx); + // Initialize components components::init(cx); // Set quit action cx.on_action(quit); - // Set app state - UserState::set_global(cx); - - // Refresh - cx.refresh(); - // Handle notifications - cx.foreground_executor() - .spawn(async move { - let client = get_client().await; - - // Generate a fake signature for rumor event. - // TODO: Find better way to save unsigned event to database. - let fake_sig = Signature::from_str(FAKE_SIG).unwrap(); - - client - .handle_notifications(|notification| async { + cx.background_executor() + .spawn({ + let sig = Signature::from_str(FAKE_SIG).unwrap(); + async move { + while let Ok(notification) = notifications.recv().await { #[allow(clippy::collapsible_match)] if let RelayPoolNotification::Message { message, .. } = notification { if let RelayMessage::Event { event, .. } = message { @@ -96,36 +94,31 @@ async fn main() { if let Ok(UnwrappedGift { rumor, .. }) = client.unwrap_gift_wrap(&event).await { - println!("rumor: {}", rumor.as_json()); let mut rumor_clone = rumor.clone(); // Compute event id if not exist rumor_clone.ensure_id(); - let ev = Event::new( - rumor_clone.id.expect("System error"), - rumor_clone.pubkey, - rumor_clone.created_at, - rumor_clone.kind, - rumor_clone.tags, - rumor_clone.content, - fake_sig, - ); + if let Some(id) = rumor_clone.id { + let ev = Event::new( + id, + rumor_clone.pubkey, + rumor_clone.created_at, + rumor_clone.kind, + rumor_clone.tags, + rumor_clone.content, + sig, + ); - // Save rumor to database to further query - if let Err(e) = client.database().save_event(&ev).await - { - println!("Error: {}", e) + // Save rumor to database to further query + _ = client.database().save_event(&ev).await } } - } else if event.kind == Kind::Metadata { - // TODO: handle metadata } } } - Ok(false) - }) - .await + } + } }) .detach(); diff --git a/crates/app/src/states/account.rs b/crates/app/src/states/account.rs new file mode 100644 index 0000000..2982355 --- /dev/null +++ b/crates/app/src/states/account.rs @@ -0,0 +1,60 @@ +use async_utility::task::spawn; +use gpui::*; +use nostr_sdk::prelude::*; +use std::time::Duration; + +use crate::{constants::SUBSCRIPTION_ID, get_client}; + +pub struct AccountState { + pub in_use: Option, +} + +impl Global for AccountState {} + +impl AccountState { + pub fn set_global(cx: &mut AppContext) { + cx.set_global(Self::new()); + + cx.observe_global::(|cx| { + let state = cx.global::(); + + if let Some(public_key) = state.in_use { + let client = get_client(); + + // Create a filter for getting all gift wrapped events send to current user + let all_messages = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); + + // Subscription options + let opts = SubscribeAutoCloseOptions::default().filter( + FilterOptions::WaitDurationAfterEOSE(Duration::from_secs(10)), + ); + + let subscription_id = SubscriptionId::new(SUBSCRIPTION_ID); + + // Create a filter for getting new message + let new_message = Filter::new() + .kind(Kind::GiftWrap) + .pubkey(public_key) + .limit(0); + + spawn(async move { + if client + .subscribe(vec![all_messages], Some(opts)) + .await + .is_ok() + { + // Subscribe for new message + _ = client + .subscribe_with_id(subscription_id, vec![new_message], None) + .await + } + }); + } + }) + .detach(); + } + + fn new() -> Self { + Self { in_use: None } + } +} diff --git a/crates/app/src/states/mod.rs b/crates/app/src/states/mod.rs index 6a8ff5e..a4acb13 100644 --- a/crates/app/src/states/mod.rs +++ b/crates/app/src/states/mod.rs @@ -1,2 +1,2 @@ +pub mod account; pub mod room; -pub mod user; diff --git a/crates/app/src/states/room.rs b/crates/app/src/states/room.rs index 95c0cf8..2a972c9 100644 --- a/crates/app/src/states/room.rs +++ b/crates/app/src/states/room.rs @@ -1,35 +1,103 @@ +use components::theme::ActiveTheme; use gpui::*; use nostr_sdk::prelude::*; +use prelude::FluentBuilder; + +use crate::get_client; #[derive(Clone)] -pub struct RoomLastMessage { - pub content: Option, - pub time: Timestamp, +#[allow(dead_code)] +struct RoomLastMessage { + content: Option, + time: Timestamp, } #[derive(Clone, IntoElement)] pub struct Room { - members: Vec, - last_message: Option, + #[allow(dead_code)] + public_key: PublicKey, + metadata: Model>, + #[allow(dead_code)] + last_message: RoomLastMessage, } impl Room { - pub fn new(members: Vec, last_message: Option) -> Self { + pub fn new(event: Event, cx: &mut WindowContext) -> Self { + let public_key = event.pubkey; + + let last_message = RoomLastMessage { + content: Some(event.content), + time: event.created_at, + }; + + let metadata = cx.new_model(|_| None); + let async_metadata = metadata.clone(); + + let mut async_cx = cx.to_async(); + + cx.foreground_executor() + .spawn(async move { + let client = get_client(); + + let query = async_cx + .background_executor() + .spawn(async move { client.database().metadata(public_key).await }) + .await; + + if let Ok(metadata) = query { + _ = async_cx.update_model(&async_metadata, |a, b| { + *a = metadata; + b.notify(); + }); + } + }) + .detach(); + Self { - members, + public_key, + metadata, last_message, } } } impl RenderOnce for Room { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { - div().child("TODO") + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let mut content = div(); + + if let Some(metadata) = self.metadata.read(cx).as_ref() { + content = content + .flex() + .items_center() + .gap_2() + .text_sm() + .when_some(metadata.picture.clone(), |div, picture| { + div.flex_shrink_0().child( + img(picture) + .size_6() + .rounded_full() + .object_fit(ObjectFit::Cover), + ) + }) + .when_some(metadata.display_name.clone(), |div, display_name| { + div.child(display_name) + }) + } else { + content = content + .flex() + .items_center() + .gap_2() + .text_sm() + .child( + div() + .flex_shrink_0() + .size_6() + .rounded_full() + .bg(cx.theme().muted), + ) + .child("Anon") + } + + div().child(content) } } - -pub struct Rooms { - pub rooms: Vec, -} - -impl Global for Rooms {} diff --git a/crates/app/src/states/user.rs b/crates/app/src/states/user.rs deleted file mode 100644 index dea862f..0000000 --- a/crates/app/src/states/user.rs +++ /dev/null @@ -1,19 +0,0 @@ -use gpui::*; -use nostr_sdk::prelude::*; - -#[derive(Clone)] -pub struct UserState { - pub current_user: Option, -} - -impl Global for UserState {} - -impl UserState { - pub fn set_global(cx: &mut AppContext) { - cx.set_global(Self::new()); - } - - fn new() -> Self { - Self { current_user: None } - } -} diff --git a/crates/app/src/ui/app.rs b/crates/app/src/ui/app.rs index fb3b946..4d8c199 100644 --- a/crates/app/src/ui/app.rs +++ b/crates/app/src/ui/app.rs @@ -1,23 +1,16 @@ use components::{ - dock::{DockArea, DockItem, PanelStyle}, + dock::{DockArea, DockItem}, + indicator::Indicator, theme::{ActiveTheme, Theme}, - Root, TitleBar, + Root, Sizable, TitleBar, }; use gpui::*; -use itertools::Itertools; -use nostr_sdk::prelude::*; -use std::{cmp::Reverse, sync::Arc, time::Duration}; +use std::sync::Arc; -use crate::{ - get_client, - states::{ - room::{Room, RoomLastMessage, Rooms}, - user::UserState, - }, -}; +use crate::states::account::AccountState; use super::{ - block::{welcome::WelcomeBlock, BlockContainer}, + block::{rooms::Rooms, welcome::WelcomeBlock, BlockContainer}, onboarding::Onboarding, }; @@ -33,7 +26,7 @@ pub const DOCK_AREA: DockAreaTab = DockAreaTab { pub struct AppView { onboarding: View, - dock_area: View, + dock: Model>>, } impl AppView { @@ -44,75 +37,34 @@ impl AppView { }) .detach(); - // Observe UserState - // If current user is present, fetching all gift wrap events - cx.observe_global::(|_v, cx| { - let app_state = cx.global::(); - let view_id = cx.parent_view_id(); - let mut async_cx = cx.to_async(); - - if let Some(public_key) = app_state.current_user { - cx.foreground_executor() - .spawn(async move { - let client = get_client().await; - let filter = Filter::new().pubkey(public_key).kind(Kind::GiftWrap); - - let mut rumors: Vec = Vec::new(); - - if let Ok(mut rx) = client - .stream_events(vec![filter], Some(Duration::from_secs(30))) - .await - { - while let Some(event) = rx.next().await { - if let Ok(UnwrappedGift { rumor, .. }) = - client.unwrap_gift_wrap(&event).await - { - rumors.push(rumor); - }; - } - - let items = rumors - .into_iter() - .sorted_by_key(|ev| Reverse(ev.created_at)) - .filter(|ev| ev.pubkey != public_key) - .unique_by(|ev| ev.pubkey) - .map(|item| { - Room::new( - vec![item.pubkey, public_key], - Some(RoomLastMessage { - content: Some(item.content), - time: item.created_at, - }), - ) - }) - .collect::>(); - - _ = async_cx.update_global::(|state, cx| { - state.rooms = items; - cx.notify(view_id); - }); - } - }) - .detach(); - } - }) - .detach(); - // Onboarding let onboarding = cx.new_view(Onboarding::new); // Dock - let dock_area = cx.new_view(|cx| { - DockArea::new(DOCK_AREA.id, Some(DOCK_AREA.version), cx).panel_style(PanelStyle::TabBar) - }); + let dock = cx.new_model(|_| None); + let async_dock = dock.clone(); - // Set dock layout - Self::init_layout(dock_area.downgrade(), cx); + // Observe UserState + // If current user is present, fetching all gift wrap events + cx.observe_global::(move |_, cx| { + if cx.global::().in_use.is_some() { + // Setup dock area + let dock_area = + cx.new_view(|cx| DockArea::new(DOCK_AREA.id, Some(DOCK_AREA.version), cx)); - AppView { - onboarding, - dock_area, - } + // Setup dock layout + Self::init_layout(dock_area.downgrade(), cx); + + // Update dock model + cx.update_model(&async_dock, |a, b| { + *a = Some(dock_area); + b.notify(); + }); + } + }) + .detach(); + + AppView { onboarding, dock } } fn init_layout(dock_area: WeakView, cx: &mut WindowContext) { @@ -120,7 +72,12 @@ impl AppView { let left_panels = DockItem::split_with_sizes( Axis::Vertical, - vec![DockItem::tabs(vec![], None, &dock_area, cx)], + vec![DockItem::tabs( + vec![Arc::new(BlockContainer::panel::(cx))], + None, + &dock_area, + cx, + )], vec![None, None], &dock_area, cx, @@ -129,7 +86,7 @@ impl AppView { _ = dock_area.update(cx, |view, cx| { view.set_version(DOCK_AREA.version, cx); view.set_left_dock(left_panels, Some(px(260.)), true, cx); - view.set_root(dock_item, cx); + view.set_center(dock_item, cx); view.set_dock_collapsible( Edges { left: false, @@ -165,17 +122,28 @@ impl Render for AppView { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let modal_layer = Root::render_modal_layer(cx); let notification_layer = Root::render_notification_layer(cx); + let mut content = div(); - if cx.global::().current_user.is_none() { - content = content.child(self.onboarding.clone()) + if cx.global::().in_use.is_none() { + content = content.size_full().child(self.onboarding.clone()) } else { - content = content - .size_full() - .flex() - .flex_col() - .child(TitleBar::new()) - .child(self.dock_area.clone()) + #[allow(clippy::collapsible_else_if)] + if let Some(dock) = self.dock.read(cx).as_ref() { + content = content + .size_full() + .flex() + .flex_col() + .child(TitleBar::new()) + .child(dock.clone()) + } else { + content = content + .size_full() + .flex() + .items_center() + .justify_center() + .child(Indicator::new().small()) + } } div() @@ -183,7 +151,7 @@ impl Render for AppView { .text_color(cx.theme().foreground) .size_full() .child(content) - .children(modal_layer) .child(div().absolute().top_8().children(notification_layer)) + .children(modal_layer) } } diff --git a/crates/app/src/ui/block/mod.rs b/crates/app/src/ui/block/mod.rs index e464beb..c17d860 100644 --- a/crates/app/src/ui/block/mod.rs +++ b/crates/app/src/ui/block/mod.rs @@ -9,6 +9,7 @@ use components::{ use gpui::*; use prelude::FluentBuilder; +pub mod rooms; pub mod welcome; actions!(block, [PanelInfo]); diff --git a/crates/app/src/ui/block/rooms.rs b/crates/app/src/ui/block/rooms.rs new file mode 100644 index 0000000..ad0400f --- /dev/null +++ b/crates/app/src/ui/block/rooms.rs @@ -0,0 +1,157 @@ +use components::{indicator::Indicator, Sizable}; +use gpui::*; +use itertools::Itertools; +use nostr_sdk::prelude::*; +use std::{cmp::Reverse, time::Duration}; + +use super::Block; +use crate::{ + get_client, + states::{account::AccountState, room::Room}, +}; + +struct RoomList { + rooms: Vec, +} + +impl RoomList { + pub fn new(raw_events: Vec, cx: &mut ViewContext<'_, Self>) -> Self { + let rooms: Vec = raw_events + .into_iter() + .map(|event| Room::new(event, cx)) + .collect(); + + Self { rooms } + } +} + +impl Render for RoomList { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + div().flex().flex_col().gap_1().children(self.rooms.clone()) + } +} + +pub struct Rooms { + rooms: Model>>, + focus_handle: FocusHandle, +} + +impl Rooms { + pub fn view(cx: &mut WindowContext) -> View { + cx.new_view(Self::new) + } + + fn new(cx: &mut ViewContext) -> Self { + let rooms = cx.new_model(|_| None); + let async_rooms = rooms.clone(); + + if let Some(public_key) = cx.global::().in_use { + let client = get_client(); + let filter = Filter::new() + .kind(Kind::PrivateDirectMessage) + .pubkey(public_key); + + let mut async_cx = cx.to_async(); + + cx.foreground_executor() + .spawn(async move { + let events = async_cx + .background_executor() + .spawn(async move { + if let Ok(events) = client.database().query(vec![filter]).await { + events + .into_iter() + .sorted_by_key(|ev| Reverse(ev.created_at)) + .filter(|ev| ev.pubkey != public_key) + .unique_by(|ev| ev.pubkey) + .collect::>() + } else { + Vec::new() + } + }) + .await; + + // Get all public keys + let public_keys: Vec = + events.iter().map(|event| event.pubkey).collect(); + + // Calculate total public keys + let total = public_keys.len(); + + // Create subscription for metadata events + let filter = Filter::new() + .kind(Kind::Metadata) + .authors(public_keys) + .limit(total); + + let opts = SubscribeAutoCloseOptions::default() + .filter(FilterOptions::WaitDurationAfterEOSE(Duration::from_secs(2))); + + async_cx + .background_executor() + .spawn(async move { + if let Err(e) = client.subscribe(vec![filter], Some(opts)).await { + println!("Error: {}", e); + } + }) + .await; + + let view = async_cx.new_view(|cx| RoomList::new(events, cx)).unwrap(); + + _ = async_cx.update_model(&async_rooms, |a, b| { + *a = Some(view); + b.notify(); + }); + }) + .detach(); + } + + Self { + rooms, + focus_handle: cx.focus_handle(), + } + } +} + +impl Block for Rooms { + fn title() -> &'static str { + "Rooms" + } + + fn new_view(cx: &mut WindowContext) -> View { + Self::view(cx) + } + + fn zoomable() -> bool { + false + } +} + +impl FocusableView for Rooms { + fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl Render for Rooms { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let mut content = div(); + + if let Some(room_list) = self.rooms.read(cx).as_ref() { + content = content + .flex() + .flex_col() + .gap_1() + .px_2() + .child(room_list.clone()); + } else { + content = content + .w_full() + .flex() + .justify_center() + .child(Indicator::new().small()) + } + + div().child(content) + } +} diff --git a/crates/app/src/ui/block/sidebar.rs b/crates/app/src/ui/block/sidebar.rs deleted file mode 100644 index 3bff531..0000000 --- a/crates/app/src/ui/block/sidebar.rs +++ /dev/null @@ -1,160 +0,0 @@ -use components::{theme::ActiveTheme, StyledExt}; -use gpui::*; -use nostr_sdk::prelude::*; -use prelude::FluentBuilder; -use std::time::Duration; - -use super::Block; -use crate::{state::get_client, utils::ago}; - -#[derive(Clone, IntoElement)] -struct Room { - #[allow(dead_code)] - public_key: PublicKey, - message_at: Timestamp, - metadata: Model>, -} - -impl Room { - pub fn new(public_key: PublicKey, created_at: Timestamp, cx: &mut WindowContext) -> Self { - let metadata = cx.new_model(|_| None); - let async_metadata = metadata.clone(); - - let mut async_cx = cx.to_async(); - - cx.foreground_executor() - .spawn(async move { - let client = get_client().await; - let metadata = client - .fetch_metadata(public_key, Some(Duration::from_secs(2))) - .await - .unwrap(); - - async_metadata - .update(&mut async_cx, |a, b| { - *a = Some(metadata); - b.notify() - }) - .unwrap(); - }) - .detach(); - - Self { - public_key, - metadata, - message_at: created_at, - } - } -} - -impl RenderOnce for Room { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { - let ago = ago(self.message_at.as_u64()); - let metadata = match self.metadata.read(cx) { - Some(metadata) => div() - .flex() - .gap_2() - .when_some(metadata.picture.clone(), |parent, picture| { - parent.child( - img(picture) - .size_6() - .rounded_full() - .object_fit(ObjectFit::Cover), - ) - }) - .when_some(metadata.display_name.clone(), |parent, display_name| { - parent.child(display_name).font_medium() - }), - None => div() - .flex() - .gap_2() - .child(div().size_6().rounded_full().bg(cx.theme().muted)) - .child("Unnamed"), - }; - - div() - .flex() - .justify_between() - .items_center() - .px_2() - .text_sm() - .child(metadata) - .child(ago) - } -} - -struct Rooms { - rooms: Vec, -} - -impl Rooms { - pub fn new(items: Vec, cx: &mut ViewContext<'_, Self>) -> Self { - let rooms: Vec = items - .iter() - .map(|item| Room::new(item.pubkey, item.created_at, cx)) - .collect(); - - Self { rooms } - } -} - -impl Render for Rooms { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - div().flex().flex_col().gap_2().children(self.rooms.clone()) - } -} - -pub struct Sidebar { - rooms: Model>>, - focus_handle: FocusHandle, -} - -impl Sidebar { - pub fn view(cx: &mut WindowContext) -> View { - cx.new_view(Self::new) - } - - fn new(cx: &mut ViewContext) -> Self { - let rooms = cx.new_model(|_| None); - let async_rooms = rooms.clone(); - - let mut async_cx = cx.to_async(); - - Self { - rooms, - focus_handle: cx.focus_handle(), - } - } -} - -impl Block for Sidebar { - fn title() -> &'static str { - "Sidebar" - } - - fn new_view(cx: &mut WindowContext) -> View { - Self::view(cx) - } - - fn zoomable() -> bool { - false - } -} - -impl FocusableView for Sidebar { - fn focus_handle(&self, _: &gpui::AppContext) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for Sidebar { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let mut content = div(); - - if let Some(rooms) = self.rooms.read(cx).as_ref() { - content = content.child(rooms.clone()); - } - - div().pt_4().child(content) - } -} diff --git a/crates/app/src/ui/chat_space/bottom_bar.rs b/crates/app/src/ui/chat_space/bottom_bar.rs deleted file mode 100644 index adf44ba..0000000 --- a/crates/app/src/ui/chat_space/bottom_bar.rs +++ /dev/null @@ -1,96 +0,0 @@ -use client::NostrClient; -use components::theme::ActiveTheme; -use gpui::*; -use nostr_sdk::prelude::*; -use prelude::FluentBuilder; -use std::time::Duration; - -use crate::state::AppState; - -#[derive(Clone, IntoElement)] -struct Account { - #[allow(dead_code)] // TODO: remove this - public_key: PublicKey, - metadata: Model>, -} - -impl Account { - pub fn new(public_key: PublicKey, cx: &mut WindowContext) -> Self { - let client = cx.global::().client; - - let metadata = cx.new_model(|_| None); - let async_metadata = metadata.clone(); - - let mut async_cx = cx.to_async(); - - cx.foreground_executor() - .spawn(async move { - match client - .fetch_metadata(public_key, Some(Duration::from_secs(2))) - .await - { - Ok(metadata) => { - async_metadata - .update(&mut async_cx, |a, b| { - *a = Some(metadata); - b.notify() - }) - .unwrap(); - } - Err(_) => todo!(), - } - }) - .detach(); - - Self { - public_key, - metadata, - } - } -} - -impl RenderOnce for Account { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { - match self.metadata.read(cx) { - Some(metadata) => div() - .w_8() - .h_12() - .px_1() - .flex() - .items_center() - .justify_center() - .border_b_2() - .border_color(cx.theme().primary_active) - .when_some(metadata.picture.clone(), |parent, picture| { - parent.child( - img(picture) - .size_6() - .rounded_full() - .object_fit(ObjectFit::Cover), - ) - }), - None => div(), // TODO: add fallback image - } - } -} - -pub struct BottomBar {} - -impl BottomBar { - pub fn new(cx: &mut ViewContext<'_, Self>) -> BottomBar { - BottomBar {} - } -} - -impl Render for BottomBar { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - div() - .h_12() - .px_3() - .flex_shrink_0() - .flex() - .items_center() - .justify_center() - .gap_1() - } -} diff --git a/crates/app/src/ui/chat_space/mod.rs b/crates/app/src/ui/chat_space/mod.rs deleted file mode 100644 index aaa13d8..0000000 --- a/crates/app/src/ui/chat_space/mod.rs +++ /dev/null @@ -1,49 +0,0 @@ -use bottom_bar::BottomBar; -use components::{ - resizable::{h_resizable, resizable_panel, ResizablePanelGroup}, - theme::ActiveTheme, -}; -use gpui::*; -use navigation::Navigation; - -pub mod bottom_bar; -pub mod navigation; - -pub struct ChatSpace { - layout: View, -} - -impl ChatSpace { - pub fn new(cx: &mut ViewContext<'_, Self>) -> Self { - let navigation = cx.new_view(Navigation::new); - - let layout = cx.new_view(|cx| { - h_resizable(cx) - .child( - resizable_panel().size(px(260.)).content(move |cx| { - div() - .size_full() - .bg(cx.theme().side_bar_background) - .text_color(cx.theme().side_bar_foreground) - .flex() - .flex_col() - .child(navigation.clone()) - .into_any_element() - }), - cx, - ) - .child( - resizable_panel().content(|_| div().child("Content").into_any_element()), - cx, - ) - }); - - Self { layout } - } -} - -impl Render for ChatSpace { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - div().relative().size_full().child(self.layout.clone()) - } -} diff --git a/crates/app/src/ui/chat_space/navigation.rs b/crates/app/src/ui/chat_space/navigation.rs deleted file mode 100644 index 8d6ddf5..0000000 --- a/crates/app/src/ui/chat_space/navigation.rs +++ /dev/null @@ -1,86 +0,0 @@ -use components::{theme::ActiveTheme, Icon, IconName}; -use gpui::*; - -#[derive(IntoElement)] -struct NavItem { - text: SharedString, - icon: Icon, -} - -impl NavItem { - pub fn new(text: SharedString, icon: Icon) -> Self { - Self { text, icon } - } -} - -impl RenderOnce for NavItem { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { - div() - .hover(|this| { - this.bg(cx.theme().side_bar_accent) - .text_color(cx.theme().side_bar_accent_foreground) - }) - .rounded_md() - .flex() - .items_center() - .h_7() - .px_2() - .gap_2() - .child(self.icon) - .child(div().pt(px(2.)).child(self.text)) - } -} - -pub struct Navigation {} - -impl Navigation { - pub fn new(cx: &mut ViewContext<'_, Self>) -> Self { - Self {} - } -} - -impl Render for Navigation { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - div().flex_1().w_full().px_2().child(div().h_11()).child( - div().flex().flex_col().gap_4().child( - div() - .flex() - .flex_col() - .gap_1() - .text_sm() - .child(NavItem::new( - "Find".into(), - Icon::new(IconName::Search) - .path("icons/search.svg") - .size_4() - .flex_shrink_0() - .text_color(cx.theme().foreground), - )) - .child(NavItem::new( - "Messages".into(), - Icon::new(IconName::Search) - .path("icons/messages.svg") - .size_4() - .flex_shrink_0() - .text_color(cx.theme().foreground), - )) - .child(NavItem::new( - "Notifications".into(), - Icon::new(IconName::Search) - .path("icons/notifications.svg") - .size_4() - .flex_shrink_0() - .text_color(cx.theme().foreground), - )) - .child(NavItem::new( - "explore".into(), - Icon::new(IconName::Search) - .path("icons/explore.svg") - .size_4() - .flex_shrink_0() - .text_color(cx.theme().foreground), - )), - ), - ) - } -} diff --git a/crates/app/src/ui/onboarding.rs b/crates/app/src/ui/onboarding.rs index f5e8652..0c7f8cc 100644 --- a/crates/app/src/ui/onboarding.rs +++ b/crates/app/src/ui/onboarding.rs @@ -1,3 +1,4 @@ +use async_utility::task::spawn; use components::{ input::{InputEvent, TextInput}, label::Label, @@ -6,7 +7,7 @@ use gpui::*; use keyring::Entry; use nostr_sdk::prelude::*; -use crate::{constants::KEYRING_SERVICE, get_client, states::user::UserState}; +use crate::{constants::KEYRING_SERVICE, get_client, states::account::AccountState}; pub struct Onboarding { input: View, @@ -21,44 +22,41 @@ impl Onboarding { }); cx.subscribe(&input, move |_, text_input, input_event, cx| { - let mut async_cx = cx.to_async(); - let view_id = cx.parent_view_id(); - if let InputEvent::PressEnter = input_event { let content = text_input.read(cx).text().to_string(); - - if let Ok(keys) = Keys::parse(content) { - cx.foreground_executor() - .spawn(async move { - let client = get_client().await; - - let public_key = keys.public_key(); - let secret = keys.secret_key().to_secret_hex(); - - let entry = - Entry::new(KEYRING_SERVICE, &public_key.to_bech32().unwrap()) - .unwrap(); - - // Store private key to OS Keyring - let _ = entry.set_password(&secret); - - // Update signer - client.set_signer(keys).await; - - // Update view - async_cx.update_global(|state: &mut UserState, cx| { - state.current_user = Some(public_key); - cx.notify(view_id); - }) - }) - .detach(); - } + _ = Self::save_keys(&content, cx); } }) .detach(); Self { input } } + + fn save_keys(content: &str, cx: &mut ViewContext) -> anyhow::Result<(), anyhow::Error> { + let keys = Keys::parse(content)?; + + let public_key = keys.public_key(); + let bech32 = public_key.to_bech32().unwrap(); + let secret = keys.secret_key().to_secret_hex(); + + let entry = Entry::new(KEYRING_SERVICE, &bech32).unwrap(); + + // Save secret key to keyring + entry.set_password(&secret)?; + + // Update signer + spawn(async move { + get_client().set_signer(keys).await; + }); + + // Update view + cx.update_global(|state: &mut AccountState, cx| { + state.in_use = Some(public_key); + cx.notify(); + }); + + Ok(()) + } } impl Render for Onboarding {