diff --git a/Cargo.toml b/Cargo.toml index 7b15e19..9218e2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] members = ["crates/*"] -default-members = ["crates/ui"] +default-members = ["crates/app"] resolver = "2" [workspace.dependencies] diff --git a/crates/ui/Cargo.toml b/crates/app/Cargo.toml similarity index 86% rename from crates/ui/Cargo.toml rename to crates/app/Cargo.toml index 2af1b2f..7370d9e 100644 --- a/crates/ui/Cargo.toml +++ b/crates/app/Cargo.toml @@ -20,7 +20,9 @@ keyring.workspace = true anyhow.workspace = true serde.workspace = true serde_json.workspace = true +itertools.workspace = true +chrono.workspace = true +dirs.workspace = true -client = { version = "0.1.0", path = "../client" } -rust-embed = "8.5.0" tracing-subscriber = { version = "0.3.18", features = ["fmt"] } +rust-embed = "8.5.0" diff --git a/crates/ui/src/asset.rs b/crates/app/src/asset.rs similarity index 100% rename from crates/ui/src/asset.rs rename to crates/app/src/asset.rs diff --git a/crates/app/src/constants.rs b/crates/app/src/constants.rs new file mode 100644 index 0000000..4096e93 --- /dev/null +++ b/crates/app/src/constants.rs @@ -0,0 +1,3 @@ +pub const KEYRING_SERVICE: &str = "Coop Safe Storage"; +pub const APP_NAME: &str = "coop"; +pub const FAKE_SIG: &str = "f9e79d141c004977192d05a86f81ec7c585179c371f7350a5412d33575a2a356433f58e405c2296ed273e2fe0aafa25b641e39cc4e1f3f261ebf55bce0cbac83"; diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs new file mode 100644 index 0000000..4bc998d --- /dev/null +++ b/crates/app/src/main.rs @@ -0,0 +1,156 @@ +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 states::user::UserState; +use ui::app::AppView; + +pub mod asset; +pub mod constants; +pub mod states; +pub mod ui; +pub mod utils; + +actions!(main_menu, [Quit]); + +pub static CLIENT: OnceCell = OnceCell::const_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/")); + + // 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)); + + // 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; + + 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 +} + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + + // Initialize nostr client + let _client = get_client().await; + + App::new() + .with_assets(Assets) + .with_http_client(Arc::new(reqwest_client::ReqwestClient::new())) + .run(move |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 { + #[allow(clippy::collapsible_match)] + if let RelayPoolNotification::Message { message, .. } = notification { + if let RelayMessage::Event { event, .. } = message { + if event.kind == Kind::GiftWrap { + 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, + ); + + // Save rumor to database to further query + if let Err(e) = client.database().save_event(&ev).await + { + println!("Error: {}", e) + } + } + } else if event.kind == Kind::Metadata { + // TODO: handle metadata + } + } + } + Ok(false) + }) + .await + }) + .detach(); + + // Set window size + let bounds = Bounds::centered(None, size(px(900.0), px(680.0)), cx); + + let opts = WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + window_decorations: Some(WindowDecorations::Client), + titlebar: Some(TitlebarOptions { + title: Some(SharedString::new_static(APP_NAME)), + appears_transparent: true, + traffic_light_position: Some(point(px(9.0), px(9.0))), + }), + ..Default::default() + }; + + cx.open_window(opts, |cx| { + let app_view = cx.new_view(AppView::new); + cx.new_view(|cx| Root::new(app_view.into(), cx)) + }) + .unwrap(); + }); +} + +fn quit(_: &Quit, cx: &mut AppContext) { + cx.quit(); +} diff --git a/crates/app/src/states/mod.rs b/crates/app/src/states/mod.rs new file mode 100644 index 0000000..6a8ff5e --- /dev/null +++ b/crates/app/src/states/mod.rs @@ -0,0 +1,2 @@ +pub mod room; +pub mod user; diff --git a/crates/app/src/states/room.rs b/crates/app/src/states/room.rs new file mode 100644 index 0000000..95c0cf8 --- /dev/null +++ b/crates/app/src/states/room.rs @@ -0,0 +1,35 @@ +use gpui::*; +use nostr_sdk::prelude::*; + +#[derive(Clone)] +pub struct RoomLastMessage { + pub content: Option, + pub time: Timestamp, +} + +#[derive(Clone, IntoElement)] +pub struct Room { + members: Vec, + last_message: Option, +} + +impl Room { + pub fn new(members: Vec, last_message: Option) -> Self { + Self { + members, + last_message, + } + } +} + +impl RenderOnce for Room { + fn render(self, _cx: &mut WindowContext) -> impl IntoElement { + div().child("TODO") + } +} + +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 new file mode 100644 index 0000000..dea862f --- /dev/null +++ b/crates/app/src/states/user.rs @@ -0,0 +1,19 @@ +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 new file mode 100644 index 0000000..fb3b946 --- /dev/null +++ b/crates/app/src/ui/app.rs @@ -0,0 +1,189 @@ +use components::{ + dock::{DockArea, DockItem, PanelStyle}, + theme::{ActiveTheme, Theme}, + Root, TitleBar, +}; +use gpui::*; +use itertools::Itertools; +use nostr_sdk::prelude::*; +use std::{cmp::Reverse, sync::Arc, time::Duration}; + +use crate::{ + get_client, + states::{ + room::{Room, RoomLastMessage, Rooms}, + user::UserState, + }, +}; + +use super::{ + block::{welcome::WelcomeBlock, BlockContainer}, + onboarding::Onboarding, +}; + +pub struct DockAreaTab { + id: &'static str, + version: usize, +} + +pub const DOCK_AREA: DockAreaTab = DockAreaTab { + id: "dock", + version: 1, +}; + +pub struct AppView { + onboarding: View, + dock_area: View, +} + +impl AppView { + pub fn new(cx: &mut ViewContext<'_, Self>) -> AppView { + // Sync theme with system + cx.observe_window_appearance(|_, cx| { + Theme::sync_system_appearance(cx); + }) + .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) + }); + + // Set dock layout + Self::init_layout(dock_area.downgrade(), cx); + + AppView { + onboarding, + dock_area, + } + } + + fn init_layout(dock_area: WeakView, cx: &mut WindowContext) { + let dock_item = Self::init_dock_items(&dock_area, cx); + + let left_panels = DockItem::split_with_sizes( + Axis::Vertical, + vec![DockItem::tabs(vec![], None, &dock_area, cx)], + vec![None, None], + &dock_area, + cx, + ); + + _ = 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_dock_collapsible( + Edges { + left: false, + ..Default::default() + }, + cx, + ); + // TODO: support right dock? + // TODO: support bottom dock? + }); + } + + fn init_dock_items(dock_area: &WeakView, cx: &mut WindowContext) -> DockItem { + DockItem::split_with_sizes( + Axis::Vertical, + vec![DockItem::tabs( + vec![ + Arc::new(BlockContainer::panel::(cx)), + // TODO: add chat block + ], + None, + dock_area, + cx, + )], + vec![None], + dock_area, + cx, + ) + } +} + +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()) + } else { + content = content + .size_full() + .flex() + .flex_col() + .child(TitleBar::new()) + .child(self.dock_area.clone()) + } + + div() + .bg(cx.theme().background) + .text_color(cx.theme().foreground) + .size_full() + .child(content) + .children(modal_layer) + .child(div().absolute().top_8().children(notification_layer)) + } +} diff --git a/crates/ui/src/views/block/mod.rs b/crates/app/src/ui/block/mod.rs similarity index 100% rename from crates/ui/src/views/block/mod.rs rename to crates/app/src/ui/block/mod.rs diff --git a/crates/app/src/ui/block/sidebar.rs b/crates/app/src/ui/block/sidebar.rs new file mode 100644 index 0000000..3bff531 --- /dev/null +++ b/crates/app/src/ui/block/sidebar.rs @@ -0,0 +1,160 @@ +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/ui/src/views/block/welcome.rs b/crates/app/src/ui/block/welcome.rs similarity index 65% rename from crates/ui/src/views/block/welcome.rs rename to crates/app/src/ui/block/welcome.rs index 8154d75..0963fde 100644 --- a/crates/ui/src/views/block/welcome.rs +++ b/crates/app/src/ui/block/welcome.rs @@ -1,3 +1,7 @@ +use components::{ + theme::{ActiveTheme, Colorize}, + StyledExt, +}; use gpui::*; use super::Block; @@ -7,15 +11,15 @@ pub struct WelcomeBlock { } impl WelcomeBlock { + pub fn view(cx: &mut WindowContext) -> View { + cx.new_view(Self::new) + } + fn new(cx: &mut ViewContext) -> Self { Self { focus_handle: cx.focus_handle(), } } - - pub fn view(cx: &mut WindowContext) -> View { - cx.new_view(Self::new) - } } impl Block for WelcomeBlock { @@ -39,7 +43,15 @@ impl FocusableView for WelcomeBlock { } impl Render for WelcomeBlock { - fn render(&mut self, _cx: &mut gpui::ViewContext) -> impl IntoElement { - div().child("Welcome") + fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { + div() + .size_full() + .flex() + .items_center() + .justify_center() + .child("coop on nostr.") + .text_color(cx.theme().muted.darken(0.1)) + .font_black() + .text_sm() } } diff --git a/crates/ui/src/views/chat_space/bottom_bar.rs b/crates/app/src/ui/chat_space/bottom_bar.rs similarity index 100% rename from crates/ui/src/views/chat_space/bottom_bar.rs rename to crates/app/src/ui/chat_space/bottom_bar.rs diff --git a/crates/ui/src/views/chat_space/mod.rs b/crates/app/src/ui/chat_space/mod.rs similarity index 100% rename from crates/ui/src/views/chat_space/mod.rs rename to crates/app/src/ui/chat_space/mod.rs diff --git a/crates/ui/src/views/chat_space/navigation.rs b/crates/app/src/ui/chat_space/navigation.rs similarity index 100% rename from crates/ui/src/views/chat_space/navigation.rs rename to crates/app/src/ui/chat_space/navigation.rs diff --git a/crates/ui/src/views/mod.rs b/crates/app/src/ui/mod.rs similarity index 100% rename from crates/ui/src/views/mod.rs rename to crates/app/src/ui/mod.rs diff --git a/crates/ui/src/views/onboarding.rs b/crates/app/src/ui/onboarding.rs similarity index 73% rename from crates/ui/src/views/onboarding.rs rename to crates/app/src/ui/onboarding.rs index bb139ef..f5e8652 100644 --- a/crates/ui/src/views/onboarding.rs +++ b/crates/app/src/ui/onboarding.rs @@ -1,4 +1,3 @@ -use ::client::NostrClient; use components::{ input::{InputEvent, TextInput}, label::Label, @@ -7,7 +6,7 @@ use gpui::*; use keyring::Entry; use nostr_sdk::prelude::*; -use crate::{constants::KEYRING_SERVICE, state::AppState}; +use crate::{constants::KEYRING_SERVICE, get_client, states::user::UserState}; pub struct Onboarding { input: View, @@ -23,7 +22,6 @@ impl Onboarding { cx.subscribe(&input, move |_, text_input, input_event, cx| { let mut async_cx = cx.to_async(); - let client = cx.global::().client; let view_id = cx.parent_view_id(); if let InputEvent::PressEnter = input_event { @@ -32,6 +30,8 @@ impl Onboarding { 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(); @@ -46,8 +46,8 @@ impl Onboarding { client.set_signer(keys).await; // Update view - async_cx.update_global(|app_state: &mut AppState, cx| { - app_state.signer = Some(public_key); + async_cx.update_global(|state: &mut UserState, cx| { + state.current_user = Some(public_key); cx.notify(view_id); }) }) @@ -64,11 +64,18 @@ impl Onboarding { impl Render for Onboarding { fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { div() - .size_1_3() + .size_full() .flex() - .flex_col() - .gap_1() - .child(Label::new("Private Key").text_sm()) - .child(self.input.clone()) + .items_center() + .justify_center() + .child( + div() + .size_1_3() + .flex() + .flex_col() + .gap_1() + .child(Label::new("Private Key").text_sm()) + .child(self.input.clone()), + ) } } diff --git a/crates/ui/src/utils.rs b/crates/app/src/utils.rs similarity index 57% rename from crates/ui/src/utils.rs rename to crates/app/src/utils.rs index cff89f4..14651f6 100644 --- a/crates/ui/src/utils.rs +++ b/crates/app/src/utils.rs @@ -1,3 +1,4 @@ +use chrono::{Local, TimeZone}; use keyring_search::{Limit, List, Search}; use nostr_sdk::prelude::*; @@ -13,3 +14,16 @@ pub fn get_all_accounts_from_keyring() -> Vec { accounts } + +pub fn ago(time: u64) -> String { + let now = Local::now(); + let input_time = Local.timestamp_opt(time as i64, 0).unwrap(); + let diff = (now - input_time).num_hours(); + + if diff < 24 { + let duration = now.signed_duration_since(input_time); + format!("{} ago", duration.num_hours()) + } else { + input_time.format("%b %d").to_string() + } +} diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml deleted file mode 100644 index cdbc61d..0000000 --- a/crates/client/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "client" -version = "0.1.0" -edition = "2021" - -[dependencies] -gpui.workspace = true -nostr-sdk.workspace = true -dirs.workspace = true -tokio.workspace = true diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs deleted file mode 100644 index f4e4a72..0000000 --- a/crates/client/src/lib.rs +++ /dev/null @@ -1,20 +0,0 @@ -use gpui::Global; -use nostr_sdk::prelude::*; -use state::get_client; - -pub mod state; - -pub struct NostrClient { - pub client: &'static Client, -} - -impl Global for NostrClient {} - -impl NostrClient { - pub async fn init() -> Self { - // Initialize nostr client - let client = get_client().await; - - Self { client } - } -} diff --git a/crates/client/src/state.rs b/crates/client/src/state.rs deleted file mode 100644 index 6331e81..0000000 --- a/crates/client/src/state.rs +++ /dev/null @@ -1,39 +0,0 @@ -use dirs::config_dir; -use nostr_sdk::prelude::*; -use std::{fs, time::Duration}; -use tokio::sync::OnceCell; - -pub static CLIENT: OnceCell = OnceCell::const_new(); - -pub async fn get_client() -> &'static Client { - CLIENT - .get_or_init(|| async { - // Setup app data folder - let config_dir = config_dir().unwrap(); - 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 Nostr Client - let opts = Options::new().gossip(true).timeout(Duration::from_secs(5)); - 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; - let _ = client.add_relay("wss://nos.lol").await; - let _ = client.add_relay("wss://directory.yabu.me").await; - - let _ = client.add_discovery_relay("wss://user.kindpag.es/").await; - let _ = client.add_discovery_relay("wss://purplepag.es").await; - - // Connect to all relays - client.connect().await; - - // Return client - client - }) - .await -} diff --git a/crates/ui/src/constants.rs b/crates/ui/src/constants.rs deleted file mode 100644 index 828de55..0000000 --- a/crates/ui/src/constants.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub const KEYRING_SERVICE: &str = "Coop Safe Storage"; -pub const APP_NAME: &str = "coop"; diff --git a/crates/ui/src/main.rs b/crates/ui/src/main.rs deleted file mode 100644 index 346ab19..0000000 --- a/crates/ui/src/main.rs +++ /dev/null @@ -1,95 +0,0 @@ -use asset::Assets; -use client::NostrClient; -use constants::{APP_NAME, KEYRING_SERVICE}; -use gpui::*; -use keyring::Entry; -use nostr_sdk::prelude::*; -use state::AppState; -use std::sync::Arc; -use utils::get_all_accounts_from_keyring; -use views::app::AppView; - -pub mod asset; -pub mod constants; -pub mod state; -pub mod utils; -pub mod views; - -actions!(main_menu, [Quit]); - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt::init(); - - // Initialize nostr client - let nostr = NostrClient::init().await; - // Initialize app state - let app_state = AppState::new(); - - App::new() - .with_assets(Assets) - .with_http_client(Arc::new(reqwest_client::ReqwestClient::new())) - .run(move |cx| { - // Initialize components - components::init(cx); - - // Set global state - cx.set_global(nostr); - cx.set_global(app_state); - - // Set quit action - cx.on_action(quit); - - // Refresh - cx.refresh(); - - // Login - let async_cx = cx.to_async(); - cx.foreground_executor() - .spawn(async move { - let accounts = get_all_accounts_from_keyring(); - - if let Some(account) = accounts.first() { - let client = async_cx - .read_global(|nostr: &NostrClient, _cx| nostr.client) - .unwrap(); - let entry = - Entry::new(KEYRING_SERVICE, account.to_bech32().unwrap().as_ref()) - .unwrap(); - let password = entry.get_password().unwrap(); - let keys = Keys::parse(password).unwrap(); - - client.set_signer(keys).await; - - async_cx - .update_global(|app_state: &mut AppState, _cx| { - app_state.signer = Some(*account); - }) - .unwrap(); - } - }) - .detach(); - - // Set window size - let bounds = Bounds::centered(None, size(px(900.0), px(680.0)), cx); - - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - window_decorations: Some(WindowDecorations::Client), - titlebar: Some(TitlebarOptions { - title: Some(SharedString::new_static(APP_NAME)), - appears_transparent: true, - traffic_light_position: Some(point(px(9.0), px(9.0))), - }), - ..Default::default() - }, - |cx| cx.new_view(AppView::new), - ) - .unwrap(); - }); -} - -fn quit(_: &Quit, cx: &mut AppContext) { - cx.quit(); -} diff --git a/crates/ui/src/state.rs b/crates/ui/src/state.rs deleted file mode 100644 index 1c0e09e..0000000 --- a/crates/ui/src/state.rs +++ /dev/null @@ -1,21 +0,0 @@ -use gpui::Global; -use nostr_sdk::prelude::*; - -pub struct AppState { - pub signer: Option, - // TODO: add more app state -} - -impl Global for AppState {} - -impl AppState { - pub fn new() -> Self { - Self { signer: None } - } -} - -impl Default for AppState { - fn default() -> Self { - Self::new() - } -} diff --git a/crates/ui/src/views/app.rs b/crates/ui/src/views/app.rs deleted file mode 100644 index 2a127e5..0000000 --- a/crates/ui/src/views/app.rs +++ /dev/null @@ -1,110 +0,0 @@ -use std::sync::Arc; - -use components::{ - dock::{DockArea, DockItem}, - theme::{ActiveTheme, Theme}, - TitleBar, -}; -use gpui::*; - -use super::{ - block::{welcome::WelcomeBlock, BlockContainer}, - onboarding::Onboarding, -}; -use crate::state::AppState; - -pub struct DockAreaTab { - id: &'static str, - version: usize, -} - -pub const DOCK_AREA: DockAreaTab = DockAreaTab { - id: "dock", - version: 1, -}; - -pub struct AppView { - onboarding: View, - dock_area: View, -} - -impl AppView { - pub fn new(cx: &mut ViewContext<'_, Self>) -> AppView { - // Sync theme with system - cx.observe_window_appearance(|_, cx| { - Theme::sync_system_appearance(cx); - }) - .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)); - let weak_dock_area = dock_area.downgrade(); - - // Set dock layout - Self::init_layout(weak_dock_area, cx); - - AppView { - onboarding, - dock_area, - } - } - - fn init_layout(dock_area: WeakView, cx: &mut WindowContext) { - let dock_item = Self::init_dock_items(&dock_area, cx); - let left_panels = - DockItem::split_with_sizes(Axis::Vertical, vec![], vec![None, None], &dock_area, cx); - - _ = 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); - // TODO: support right dock? - // TODO: support bottom dock? - }); - } - - fn init_dock_items(dock_area: &WeakView, cx: &mut WindowContext) -> DockItem { - DockItem::split_with_sizes( - Axis::Vertical, - vec![DockItem::tabs( - vec![ - Arc::new(BlockContainer::panel::(cx)), - Arc::new(BlockContainer::panel::(cx)), - // TODO: add chat block - ], - None, - dock_area, - cx, - )], - vec![None], - dock_area, - cx, - ) - } -} - -impl Render for AppView { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let mut content = div(); - - if cx.global::().signer.is_none() { - content = content.child(self.onboarding.clone()) - } else { - content = content - .size_full() - .flex() - .flex_col() - .child(TitleBar::new()) - .child(self.dock_area.clone()) - } - - div() - .bg(cx.theme().background) - .text_color(cx.theme().foreground) - .size_full() - .child(content) - } -}