From a61fd27b9d3dc6221e619216430e9de4b1c11ec5 Mon Sep 17 00:00:00 2001 From: reya Date: Sat, 1 Feb 2025 08:30:24 +0700 Subject: [PATCH] wip: refactor --- Cargo.lock | 72 ++- Cargo.toml | 1 + crates/app/Cargo.toml | 4 +- crates/app/src/main.rs | 445 ++++++++++-------- crates/app/src/views/app.rs | 121 ++--- crates/app/src/views/chat/message.rs | 6 +- crates/app/src/views/chat/mod.rs | 4 +- crates/app/src/views/mod.rs | 3 +- crates/app/src/views/onboarding/mod.rs | 11 +- crates/app/src/views/sidebar/compose.rs | 32 +- crates/app/src/views/sidebar/inbox.rs | 3 +- crates/app/src/views/sidebar/mod.rs | 2 +- crates/app/src/views/startup.rs | 26 + crates/app_state/Cargo.toml | 13 + crates/app_state/src/lib.rs | 1 + .../src/app.rs => app_state/src/registry.rs} | 59 +-- crates/{registry => chat}/Cargo.toml | 3 +- crates/chat/src/inbox.rs | 59 +++ crates/chat/src/lib.rs | 3 + .../src/chat.rs => chat/src/registry.rs} | 68 +-- crates/{registry => chat}/src/room.rs | 25 +- crates/common/src/lib.rs | 1 + .../src/contact.rs => common/src/profile.rs} | 11 +- crates/registry/src/lib.rs | 4 - crates/ui/src/root.rs | 77 +-- 25 files changed, 547 insertions(+), 507 deletions(-) create mode 100644 crates/app/src/views/startup.rs create mode 100644 crates/app_state/Cargo.toml create mode 100644 crates/app_state/src/lib.rs rename crates/{registry/src/app.rs => app_state/src/registry.rs} (58%) rename crates/{registry => chat}/Cargo.toml (86%) create mode 100644 crates/chat/src/inbox.rs create mode 100644 crates/chat/src/lib.rs rename crates/{registry/src/chat.rs => chat/src/registry.rs} (69%) rename crates/{registry => chat}/src/room.rs (83%) rename crates/{registry/src/contact.rs => common/src/profile.rs} (86%) delete mode 100644 crates/registry/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 34c4c0c..2ef1775 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,6 +138,17 @@ version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +[[package]] +name = "app_state" +version = "0.1.0" +dependencies = [ + "common", + "gpui", + "nostr-sdk", + "state", + "ui", +] + [[package]] name = "arbitrary" version = "1.4.1" @@ -865,6 +876,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "chat" +version = "0.1.0" +dependencies = [ + "anyhow", + "common", + "gpui", + "itertools 0.13.0", + "nostr-sdk", + "smol", + "state", +] + [[package]] name = "chrono" version = "0.4.39" @@ -1014,7 +1038,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#19383036d5ec1ac7821ad21b3ff1fae95a5f004e" +source = "git+https://github.com/zed-industries/zed#e1af35aa154ddc10faf4e9a5aac37ae543b1f6ac" dependencies = [ "indexmap", "rustc-hash 2.1.0", @@ -1084,14 +1108,16 @@ name = "coop" version = "0.1.0" dependencies = [ "anyhow", + "app_state", + "chat", "chrono", "common", "dirs 5.0.1", "gpui", + "gpui_tokio", "itertools 0.13.0", "log", "nostr-sdk", - "registry", "reqwest_client", "rust-embed", "serde", @@ -1318,7 +1344,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#19383036d5ec1ac7821ad21b3ff1fae95a5f004e" +source = "git+https://github.com/zed-industries/zed#e1af35aa154ddc10faf4e9a5aac37ae543b1f6ac" dependencies = [ "proc-macro2", "quote", @@ -2031,7 +2057,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#19383036d5ec1ac7821ad21b3ff1fae95a5f004e" +source = "git+https://github.com/zed-industries/zed#e1af35aa154ddc10faf4e9a5aac37ae543b1f6ac" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2118,13 +2144,23 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#19383036d5ec1ac7821ad21b3ff1fae95a5f004e" +source = "git+https://github.com/zed-industries/zed#e1af35aa154ddc10faf4e9a5aac37ae543b1f6ac" dependencies = [ "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "gpui_tokio" +version = "0.1.0" +source = "git+https://github.com/zed-industries/zed#e1af35aa154ddc10faf4e9a5aac37ae543b1f6ac" +dependencies = [ + "gpui", + "tokio", + "util", +] + [[package]] name = "grid" version = "0.13.0" @@ -2323,7 +2359,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#19383036d5ec1ac7821ad21b3ff1fae95a5f004e" +source = "git+https://github.com/zed-industries/zed#e1af35aa154ddc10faf4e9a5aac37ae543b1f6ac" dependencies = [ "anyhow", "bytes", @@ -2985,7 +3021,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#19383036d5ec1ac7821ad21b3ff1fae95a5f004e" +source = "git+https://github.com/zed-industries/zed#e1af35aa154ddc10faf4e9a5aac37ae543b1f6ac" dependencies = [ "anyhow", "bindgen", @@ -4236,7 +4272,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#19383036d5ec1ac7821ad21b3ff1fae95a5f004e" +source = "git+https://github.com/zed-industries/zed#e1af35aa154ddc10faf4e9a5aac37ae543b1f6ac" dependencies = [ "derive_refineable", ] @@ -4270,18 +4306,6 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" -[[package]] -name = "registry" -version = "0.1.0" -dependencies = [ - "anyhow", - "common", - "gpui", - "itertools 0.13.0", - "nostr-sdk", - "state", -] - [[package]] name = "reqwest" version = "0.12.8" @@ -4377,7 +4401,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#19383036d5ec1ac7821ad21b3ff1fae95a5f004e" +source = "git+https://github.com/zed-industries/zed#e1af35aa154ddc10faf4e9a5aac37ae543b1f6ac" dependencies = [ "anyhow", "bytes", @@ -4727,7 +4751,7 @@ checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" [[package]] name = "semantic_version" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#19383036d5ec1ac7821ad21b3ff1fae95a5f004e" +source = "git+https://github.com/zed-industries/zed#e1af35aa154ddc10faf4e9a5aac37ae543b1f6ac" dependencies = [ "anyhow", "serde", @@ -5052,7 +5076,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#19383036d5ec1ac7821ad21b3ff1fae95a5f004e" +source = "git+https://github.com/zed-industries/zed#e1af35aa154ddc10faf4e9a5aac37ae543b1f6ac" dependencies = [ "arrayvec", "log", @@ -5874,7 +5898,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#19383036d5ec1ac7821ad21b3ff1fae95a5f004e" +source = "git+https://github.com/zed-industries/zed#e1af35aa154ddc10faf4e9a5aac37ae543b1f6ac" dependencies = [ "anyhow", "async-fs", diff --git a/Cargo.toml b/Cargo.toml index c009ef8..4cbe4a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ coop = { path = "crates/*" } # UI gpui = { git = "https://github.com/zed-industries/zed" } +gpui_tokio = { git = "https://github.com/zed-industries/zed" } reqwest_client = { git = "https://github.com/zed-industries/zed" } # Nostr diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 5a4f87f..9b930c4 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -12,9 +12,11 @@ path = "src/main.rs" ui = { path = "../ui" } common = { path = "../common" } state = { path = "../state" } -registry = { path = "../registry" } +app_state = { path = "../app_state" } +chat = { path = "../chat" } gpui.workspace = true +gpui_tokio.workspace = true reqwest_client.workspace = true tokio.workspace = true diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index b8b61db..206d1db 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -1,25 +1,26 @@ +use app_state::registry::AppRegistry; use asset::Assets; -use common::constants::{ - ALL_MESSAGES_SUB_ID, APP_ID, APP_NAME, FAKE_SIG, KEYRING_SERVICE, METADATA_DELAY, - NEW_MESSAGE_SUB_ID, +use async_utility::task::spawn; +use chat::registry::ChatRegistry; +use common::{ + constants::{ + ALL_MESSAGES_SUB_ID, APP_ID, APP_NAME, FAKE_SIG, KEYRING_SERVICE, NEW_MESSAGE_SUB_ID, + }, + profile::NostrProfile, }; use gpui::{ - actions, point, px, size, App, AppContext, Application, Bounds, SharedString, TitlebarOptions, - WindowBounds, WindowKind, WindowOptions, + actions, point, px, size, App, AppContext, Application, BorrowAppContext, Bounds, SharedString, + TitlebarOptions, WindowBounds, WindowKind, WindowOptions, }; #[cfg(target_os = "linux")] use gpui::{WindowBackgroundAppearance, WindowDecorations}; -use log::warn; +use log::{error, info}; use nostr_sdk::prelude::*; -use registry::{app::AppRegistry, chat::ChatRegistry, contact::Contact}; use state::{get_client, initialize_client}; -use std::{collections::HashSet, str::FromStr, sync::Arc, time::Duration}; -use tokio::{ - sync::{mpsc, Mutex}, - time::sleep, -}; +use std::{collections::HashSet, ops::Deref, str::FromStr, sync::Arc, time::Duration}; +use tokio::sync::mpsc; use ui::Root; -use views::app::AppView; +use views::{app::AppView, onboarding::Onboarding, startup::Startup}; mod asset; mod views; @@ -34,55 +35,132 @@ pub enum Signal { Eose, } -#[tokio::main] -async fn main() { +fn main() { + // Log tracing_subscriber::fmt::init(); - // Initialize client + // Initialize nostr client initialize_client(); // Get client let client = get_client(); + let (signal_tx, mut signal_rx) = tokio::sync::mpsc::channel::(4096); - // 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; + spawn(async move { + // 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; + _ = 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; + // Connect to all relays + _ = client.connect().await + }); - // Signal - let (signal_tx, mut signal_rx) = mpsc::channel::(4096); - let (mta_tx, mta_rx) = mpsc::channel::(4096); + spawn(async move { + let (batch_tx, mut batch_rx) = mpsc::channel::>(20); - // Handle notifications from relays - // Send notify back to GPUI - tokio::spawn(async move { - let mut notifications = client.notifications(); + async fn sync_metadata(client: &Client, buffer: &HashSet) { + let filter = Filter::new() + .authors(buffer.iter().copied()) + .kind(Kind::Metadata) + .limit(buffer.len()); + + if let Err(e) = client.sync(filter, &SyncOptions::default()).await { + error!("NEG error: {e}"); + } + } + + async fn process_batch(client: &Client, events: &[Box]) { + let sig = Signature::from_str(FAKE_SIG).unwrap(); + let mut buffer: HashSet = HashSet::with_capacity(100); + + for event in events.iter() { + if let Ok(UnwrappedGift { mut rumor, sender }) = + client.unwrap_gift_wrap(event).await + { + let pubkeys: HashSet = event.tags.public_keys().copied().collect(); + buffer.extend(pubkeys); + buffer.insert(sender); + + // Create event's ID is not exist + rumor.ensure_id(); + + // Save event to database + if let Some(id) = rumor.id { + let ev = Event::new( + id, + rumor.pubkey, + rumor.created_at, + rumor.kind, + rumor.tags, + rumor.content, + sig, + ); + + if let Err(e) = client.database().save_event(&ev).await { + error!("Save error: {}", e); + } + } + } + } + + sync_metadata(client, &buffer).await; + } + + // Spawn a thread to handle batch process + spawn(async move { + const BATCH_SIZE: usize = 20; + const BATCH_TIMEOUT: Duration = Duration::from_millis(200); + + let mut batch = Vec::with_capacity(20); + let mut timeout = Box::pin(tokio::time::sleep(BATCH_TIMEOUT)); + + loop { + tokio::select! { + event = batch_rx.recv() => { + if let Some(event) = event { + batch.push(event); + + if batch.len() == BATCH_SIZE { + process_batch(client, &batch).await; + batch.clear(); + timeout = Box::pin(tokio::time::sleep(BATCH_TIMEOUT)); + } + } else { + break; + } + } + _ = &mut timeout => { + if !batch.is_empty() { + process_batch(client, &batch).await; + batch.clear(); + } + timeout = Box::pin(tokio::time::sleep(BATCH_TIMEOUT)); + } + } + } + }); + + let all_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID); + let new_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID); let sig = Signature::from_str(FAKE_SIG).unwrap(); - let new_message = SubscriptionId::new(NEW_MESSAGE_SUB_ID); - let all_messages = SubscriptionId::new(ALL_MESSAGES_SUB_ID); + let mut notifications = client.notifications(); while let Ok(notification) = notifications.recv().await { if let RelayPoolNotification::Message { message, .. } = notification { - if let RelayMessage::Event { - event, - subscription_id, - } = message - { - match event.kind { + match message { + RelayMessage::Event { + event, + subscription_id, + } => match event.kind { Kind::GiftWrap => { - match client.unwrap_gift_wrap(&event).await { - Ok(UnwrappedGift { mut rumor, sender }) => { - // Request metadata - if let Err(e) = mta_tx.send(sender).await { - warn!("Send error: {}", e) - }; - + if subscription_id == new_id { + if let Ok(UnwrappedGift { mut rumor, .. }) = + client.unwrap_gift_wrap(&event).await + { // Compute event id if not exist rumor.ensure_id(); @@ -99,55 +177,47 @@ async fn main() { // Save rumor to database to further query if let Err(e) = client.database().save_event(&ev).await { - warn!("Save error: {}", e); + error!("Save error: {}", e); } - // Send event back to channel - if subscription_id == new_message { - if let Err(e) = signal_tx.send(Signal::Event(ev)).await - { - warn!("Send error: {}", e) - } + // Send new event to GPUI + if let Err(e) = signal_tx.send(Signal::Event(ev)).await { + error!("Send error: {}", e) } } } - Err(e) => warn!("Unwrap error: {}", e), + } + + if let Err(e) = batch_tx.send(event).await { + error!("Failed to add to batch: {}", e); } } Kind::ContactList => { - let public_keys: Vec = + let public_keys: HashSet<_> = event.tags.public_keys().copied().collect(); - for public_key in public_keys.into_iter() { - if let Err(e) = mta_tx.send(public_key).await { - warn!("Send error: {}", e) - }; - } + sync_metadata(client, &public_keys).await; } _ => {} + }, + RelayMessage::EndOfStoredEvents(subscription_id) => { + if subscription_id == all_id { + if let Err(e) = signal_tx.send(Signal::Eose).await { + error!("Failed to send eose: {}", e) + }; + } } - } else if let RelayMessage::EndOfStoredEvents(subscription_id) = message { - if subscription_id == all_messages { - if let Err(e) = signal_tx.send(Signal::Eose).await { - warn!("Send error: {}", e) - }; - } + _ => {} } } } }); - // Handle metadata request - // Merge all requests into single subscription - tokio::spawn(async move { handle_metadata(client, mta_rx).await }); - Application::new() .with_assets(Assets) .with_http_client(Arc::new(reqwest_client::ReqwestClient::new())) .run(move |cx| { - // App state - AppRegistry::set_global(cx); - // Chat state + // Initialize chat global state ChatRegistry::set_global(cx); // Initialize components @@ -156,81 +226,6 @@ async fn main() { // Set quit action cx.on_action(quit); - // Spawn a thread to handle Nostr events - cx.spawn(|async_cx| async move { - let (tx, rx) = smol::channel::bounded::(4096); - - async_cx - .background_executor() - .spawn(async move { - while let Some(signal) = signal_rx.recv().await { - if let Err(e) = tx.send(signal).await { - warn!("Send error: {}", e) - } - } - }) - .detach(); - - while let Ok(signal) = rx.recv().await { - match signal { - Signal::Eose => { - _ = async_cx.update_global::(|chat, cx| { - chat.load(cx); - }); - } - Signal::Event(event) => { - _ = async_cx.update_global::(|state, cx| { - state.receive(event, cx) - }); - } - } - } - }) - .detach(); - - // Spawn a thread to update Nostr signer - cx.spawn(|async_cx| { - let task = cx.read_credentials(KEYRING_SERVICE); - - async move { - if let Ok(Some((npub, secret))) = task.await { - let public_key = - PublicKey::from_bech32(&npub).expect("Public Key isn't valid."); - - let query: anyhow::Result = async_cx - .background_executor() - .spawn(async move { - let hex = String::from_utf8(secret)?; - let keys = Keys::parse(&hex)?; - - // Update signer - _ = client.set_signer(keys).await; - - // Get metadata - if let Some(metadata) = - client.database().metadata(public_key).await? - { - Ok(metadata) - } else { - Ok(Metadata::new()) - } - }) - .await; - - if let Ok(metadata) = query { - _ = async_cx.update_global::(|state, cx| { - state.set_user(Contact::new(public_key, metadata), cx); - }); - } - } else { - _ = async_cx.update_global::(|state, _| { - state.is_loading = false; - }); - } - } - }) - .detach(); - let opts = WindowOptions { #[cfg(not(target_os = "linux"))] titlebar: Some(TitlebarOptions { @@ -251,50 +246,122 @@ async fn main() { ..Default::default() }; - cx.open_window(opts, |window, cx| { - window.set_window_title(APP_NAME); - window.set_app_id(APP_ID); - cx.activate(true); - cx.new(|cx| Root::new(cx.new(|cx| AppView::new(window, cx)).into(), window, cx)) + let window = cx + .open_window(opts, |window, cx| { + cx.activate(true); + window.set_window_title(APP_NAME); + window.set_app_id(APP_ID); + + let root = cx.new(|cx| { + Root::new(cx.new(|cx| Startup::new(window, cx)).into(), window, cx) + }); + + let weak_root = root.downgrade(); + let window_handle = window.window_handle(); + let task = cx.read_credentials(KEYRING_SERVICE); + + // Initialize app global state + AppRegistry::set_global(weak_root, cx); + + cx.spawn(|mut cx| async move { + if let Ok(Some((npub, secret))) = task.await { + let (tx, mut rx) = tokio::sync::mpsc::channel::(1); + + cx.background_executor() + .spawn(async move { + let public_key = PublicKey::from_bech32(&npub).unwrap(); + let secret_hex = String::from_utf8(secret).unwrap(); + let keys = Keys::parse(&secret_hex).unwrap(); + + // Update nostr signer + _ = client.set_signer(keys).await; + + // Get user's metadata + let metadata = if let Ok(Some(metadata)) = + client.database().metadata(public_key).await + { + metadata + } else { + Metadata::new() + }; + + if tx + .send(NostrProfile::new(public_key, metadata)) + .await + .is_ok() + { + info!("Found account"); + } + }) + .detach(); + + while let Some(profile) = rx.recv().await { + cx.update_window(window_handle, |_, window, cx| { + cx.update_global::(|this, cx| { + this.set_user(Some(profile.clone())); + + if let Some(root) = this.root() { + cx.update_entity(&root, |this: &mut Root, cx| { + this.set_view( + cx.new(|cx| AppView::new(profile, window, cx)) + .into(), + cx, + ); + }); + } + }); + }) + .unwrap(); + } + } else { + cx.update_window(window_handle, |_, window, cx| { + cx.update_global::(|this, cx| { + if let Some(root) = this.root() { + cx.update_entity(&root, |this: &mut Root, cx| { + this.set_view( + cx.new(|cx| Onboarding::new(window, cx)).into(), + cx, + ); + }); + } + }); + }) + .unwrap(); + } + }) + .detach(); + + root + }) + .expect("System error. Please re-open the app."); + + // Listen for messages from the Nostr thread + cx.spawn(|mut cx| async move { + while let Some(signal) = signal_rx.recv().await { + match signal { + Signal::Eose => { + cx.update_window(*window.deref(), |_this, _window, cx| { + cx.update_global::(|this, cx| { + this.load(cx); + }); + }) + .unwrap(); + } + Signal::Event(event) => { + cx.update_window(*window.deref(), |_this, _window, cx| { + cx.update_global::(|this, cx| { + this.new_room_message(event, cx); + }); + }) + .unwrap(); + } + } + } }) - .expect("System error"); + .detach(); }); } -async fn handle_metadata(client: &'static Client, mut mta_rx: mpsc::Receiver) { - let queue: Arc>> = Arc::new(Mutex::new(HashSet::new())); - let queue_clone = Arc::clone(&queue); - - let (tx, mut rx) = mpsc::channel::(200); - - tokio::spawn(async move { - while let Some(public_key) = mta_rx.recv().await { - queue_clone.lock().await.insert(public_key); - _ = tx.send(public_key).await; - } - }); - - tokio::spawn(async move { - while rx.recv().await.is_some() { - sleep(Duration::from_millis(METADATA_DELAY)).await; - - let authors: Vec = queue.lock().await.drain().collect(); - let total = authors.len(); - - if total > 0 { - let filter = Filter::new() - .authors(authors) - .kind(Kind::Metadata) - .limit(total); - - if let Err(e) = client.sync(filter, &SyncOptions::default()).await { - warn!("Error: {}", e); - } - } - } - }); -} - fn quit(_: &Quit, cx: &mut App) { - cx.quit(); + cx.shutdown(); } diff --git a/crates/app/src/views/app.rs b/crates/app/src/views/app.rs index 30860eb..2a8e443 100644 --- a/crates/app/src/views/app.rs +++ b/crates/app/src/views/app.rs @@ -1,10 +1,12 @@ -use super::{chat::ChatPanel, onboarding::Onboarding, sidebar::Sidebar, welcome::WelcomePanel}; +use super::{chat::ChatPanel, sidebar::Sidebar, welcome::WelcomePanel}; +use app_state::registry::AppRegistry; +use chat::registry::ChatRegistry; +use common::profile::NostrProfile; use gpui::{ - actions, div, img, impl_internal_actions, px, svg, App, AppContext, Axis, BorrowAppContext, - Context, Edges, Entity, InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, - Styled, StyledImage, WeakEntity, Window, + actions, div, img, impl_internal_actions, px, AppContext, Axis, BorrowAppContext, Context, + Edges, Entity, InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, Styled, + StyledImage, Window, }; -use registry::{app::AppRegistry, chat::ChatRegistry, contact::Contact}; use serde::Deserialize; use state::get_client; use std::sync::Arc; @@ -13,8 +15,6 @@ use ui::{ dock_area::{dock::DockPlacement, DockArea, DockItem}, notification::NotificationType, popup_menu::PopupMenuExt, - prelude::FluentBuilder, - theme::{scale::ColorScaleStep, ActiveTheme}, ContextModal, Icon, IconName, Root, Sizable, TitleBar, }; @@ -45,48 +45,32 @@ pub const DOCK_AREA: DockAreaTab = DockAreaTab { }; pub struct AppView { - onboarding: Entity, + account: NostrProfile, dock: Entity, } impl AppView { - pub fn new(window: &mut Window, cx: &mut Context<'_, Self>) -> AppView { - let onboarding = cx.new(|cx| Onboarding::new(window, cx)); + pub fn new(account: NostrProfile, window: &mut Window, cx: &mut Context<'_, Self>) -> AppView { let dock = cx.new(|cx| DockArea::new(DOCK_AREA.id, Some(DOCK_AREA.version), window, cx)); + let weak_dock = dock.downgrade(); - // Get current user from app state - let weak_user = cx.global::().user(); - - if let Some(user) = weak_user.upgrade() { - cx.observe_in(&user, window, |view, this, window, cx| { - if this.read(cx).is_some() { - Self::render_dock(view.dock.downgrade(), window, cx); - } - }) - .detach(); - } - - AppView { onboarding, dock } - } - - fn render_dock(dock_area: WeakEntity, window: &mut Window, cx: &mut App) { let left = DockItem::panel(Arc::new(Sidebar::new(window, cx))); let center = DockItem::split_with_sizes( Axis::Vertical, vec![DockItem::tabs( vec![Arc::new(WelcomePanel::new(window, cx))], None, - &dock_area, + &weak_dock, window, cx, )], vec![None], - &dock_area, + &weak_dock, window, cx, ); - _ = dock_area.update(cx, |view, cx| { + _ = weak_dock.update(cx, |view, cx| { view.set_version(DOCK_AREA.version, window, cx); view.set_left_dock(left, Some(px(240.)), true, window, cx); view.set_center(center, window, cx); @@ -101,16 +85,18 @@ impl AppView { // TODO: support right dock? // TODO: support bottom dock? }); + + AppView { account, dock } } - fn render_account(&self, account: Contact) -> impl IntoElement { + fn render_account(&self) -> impl IntoElement { Button::new("account") .ghost() .xsmall() .reverse() .icon(Icon::new(IconName::ChevronDownSmall)) .child( - img(account.avatar()) + img(self.account.avatar()) .size_5() .rounded_full() .object_fit(ObjectFit::Cover), @@ -127,7 +113,7 @@ impl AppView { fn on_panel_action(&mut self, action: &AddPanel, window: &mut Window, cx: &mut Context) { match &action.panel { PanelKind::Room(id) => { - if let Some(weak_room) = cx.global::().room(id, cx) { + if let Some(weak_room) = cx.global::().get_room(id, cx) { if let Some(room) = weak_room.upgrade() { let panel = Arc::new(ChatPanel::new(room, window, cx)); @@ -175,10 +161,8 @@ impl AppView { // TODO } - fn on_logout_action(&mut self, _action: &Logout, window: &mut Window, cx: &mut Context) { - cx.update_global::(|this, cx| { - this.logout(cx); - // Reset nostr client + fn on_logout_action(&mut self, _action: &Logout, _window: &mut Window, cx: &mut Context) { + cx.update_global::(|_this, cx| { cx.background_executor() .spawn(async move { get_client().reset().await }) .detach(); @@ -190,56 +174,35 @@ impl Render for AppView { 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); - let state = cx.global::(); div() + .relative() .size_full() .flex() .flex_col() // Main - .map(|this| { - if state.is_loading { - this - // Placeholder - .child(div()) - .child( - div().flex_1().flex().items_center().justify_center().child( - svg() - .path("brand/coop.svg") - .size_12() - .text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)), - ), - ) - } else if let Some(contact) = state.current_user(window, cx) { - this.child( - TitleBar::new() - // Left side - .child(div()) - // Right side - .child( - div() - .flex() - .items_center() - .justify_end() - .gap_1() - .px_2() - .child(self.render_account(contact)), - ), - ) - .child(self.dock.clone()) - // Listener - .on_action(cx.listener(Self::on_panel_action)) - .on_action(cx.listener(Self::on_logout_action)) - .on_action(cx.listener(Self::on_profile_action)) - .on_action(cx.listener(Self::on_contacts_action)) - .on_action(cx.listener(Self::on_settings_action)) - } else { - this.child(TitleBar::new()).child(self.onboarding.clone()) - } - }) - // Notification + .child( + TitleBar::new() + // Left side + .child(div()) + // Right side + .child( + div() + .flex() + .items_center() + .justify_end() + .gap_1() + .px_2() + .child(self.render_account()), + ), + ) + .child(self.dock.clone()) .child(div().absolute().top_8().children(notification_layer)) - // Modal .children(modal_layer) + .on_action(cx.listener(Self::on_panel_action)) + .on_action(cx.listener(Self::on_logout_action)) + .on_action(cx.listener(Self::on_profile_action)) + .on_action(cx.listener(Self::on_contacts_action)) + .on_action(cx.listener(Self::on_settings_action)) } } diff --git a/crates/app/src/views/chat/message.rs b/crates/app/src/views/chat/message.rs index 9d6ac8a..dc91236 100644 --- a/crates/app/src/views/chat/message.rs +++ b/crates/app/src/views/chat/message.rs @@ -1,8 +1,8 @@ +use common::profile::NostrProfile; use gpui::{ div, img, px, App, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, }; -use registry::contact::Contact; use ui::{ theme::{scale::ColorScaleStep, ActiveTheme}, StyledExt, @@ -10,7 +10,7 @@ use ui::{ #[derive(Clone, Debug, IntoElement)] pub struct Message { - member: Contact, + member: NostrProfile, content: SharedString, ago: SharedString, } @@ -26,7 +26,7 @@ impl PartialEq for Message { } impl Message { - pub fn new(member: Contact, content: SharedString, ago: SharedString) -> Self { + pub fn new(member: NostrProfile, content: SharedString, ago: SharedString) -> Self { Self { member, content, diff --git a/crates/app/src/views/chat/mod.rs b/crates/app/src/views/chat/mod.rs index 38d1f7a..957582b 100644 --- a/crates/app/src/views/chat/mod.rs +++ b/crates/app/src/views/chat/mod.rs @@ -1,4 +1,5 @@ use async_utility::task::spawn; +use chat::room::Room; use common::{ constants::IMAGE_SERVICE, utils::{compare, message_time, nip96_upload}, @@ -12,7 +13,6 @@ use gpui::{ use itertools::Itertools; use message::Message; use nostr_sdk::prelude::*; -use registry::room::Room; use smol::fs; use state::get_client; use tokio::sync::oneshot; @@ -140,7 +140,7 @@ impl ChatPanel { let members = room.members.clone(); let owner = room.owner.clone(); // Get all public keys - let all_keys = room.get_all_keys(); + let all_keys = room.get_pubkeys(); // Async let async_state = self.state.clone(); diff --git a/crates/app/src/views/mod.rs b/crates/app/src/views/mod.rs index 4e26b09..43e4c46 100644 --- a/crates/app/src/views/mod.rs +++ b/crates/app/src/views/mod.rs @@ -1,6 +1,7 @@ mod chat; -mod onboarding; mod sidebar; mod welcome; pub mod app; +pub mod onboarding; +pub mod startup; diff --git a/crates/app/src/views/onboarding/mod.rs b/crates/app/src/views/onboarding/mod.rs index ff6321a..98f237b 100644 --- a/crates/app/src/views/onboarding/mod.rs +++ b/crates/app/src/views/onboarding/mod.rs @@ -1,9 +1,6 @@ -use common::constants::KEYRING_SERVICE; -use gpui::{ - div, App, AppContext, Context, Entity, IntoElement, ParentElement, Render, Styled, Window, -}; +use common::{constants::KEYRING_SERVICE, profile::NostrProfile}; +use gpui::{div, AppContext, Context, Entity, IntoElement, ParentElement, Render, Styled, Window}; use nostr_sdk::prelude::*; -use registry::{app::AppRegistry, contact::Contact}; use state::get_client; use ui::input::{InputEvent, TextInput}; @@ -71,9 +68,7 @@ impl Onboarding { .await; if let Ok(metadata) = query { - _ = async_cx.update_global::(|state, cx| { - state.set_user(Contact::new(public_key, metadata), cx); - }); + // } } } diff --git a/crates/app/src/views/sidebar/compose.rs b/crates/app/src/views/sidebar/compose.rs index d99bb47..16f5e1a 100644 --- a/crates/app/src/views/sidebar/compose.rs +++ b/crates/app/src/views/sidebar/compose.rs @@ -1,11 +1,15 @@ -use common::utils::{random_name, room_hash}; +use app_state::registry::AppRegistry; +use chat::room::Room; +use common::{ + profile::NostrProfile, + utils::{random_name, room_hash}, +}; use gpui::{ div, img, impl_internal_actions, px, uniform_list, App, AppContext, Context, Entity, FocusHandle, InteractiveElement, IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window, }; use nostr_sdk::prelude::*; -use registry::{app::AppRegistry, contact::Contact, room::Room}; use serde::Deserialize; use state::get_client; use std::{collections::HashSet, time::Duration}; @@ -27,7 +31,7 @@ pub struct Compose { title_input: Entity, message_input: Entity, user_input: Entity, - contacts: Entity>>, + contacts: Entity>>, selected: Entity>, focus_handle: FocusHandle, is_loading: bool, @@ -78,15 +82,17 @@ impl Compose { let client = get_client(); async move { - let query: anyhow::Result, anyhow::Error> = async_cx + let query: anyhow::Result, anyhow::Error> = async_cx .background_executor() .spawn(async move { let signer = client.signer().await?; let public_key = signer.get_public_key().await?; let profiles = client.database().contacts(public_key).await?; - let members: Vec = profiles + let members: Vec = profiles .into_iter() - .map(|profile| Contact::new(profile.public_key(), profile.metadata())) + .map(|profile| { + NostrProfile::new(profile.public_key(), profile.metadata()) + }) .collect(); Ok(members) @@ -120,10 +126,8 @@ impl Compose { } } - pub fn room(&self, window: &Window, cx: &App) -> Option { - let current_user = cx.global::().current_user(window, cx); - - if let Some(current_user) = current_user { + pub fn room(&self, _window: &Window, cx: &App) -> Option { + if let Some(current_user) = cx.global::().user() { // Convert selected pubkeys into nostr tags let tags: Vec = self .selected @@ -134,19 +138,19 @@ impl Compose { let tags = Tags::new(tags); // Convert selected pubkeys into members - let members: Vec = self + let members: Vec = self .selected .read(cx) .clone() .into_iter() - .map(|pk| Contact::new(pk, Metadata::new())) + .map(|pk| NostrProfile::new(pk, Metadata::new())) .collect(); // Get room's id let id = room_hash(&tags); // Get room's owner (current user) - let owner = Contact::new(current_user.public_key(), Metadata::new()); + let owner = NostrProfile::new(current_user.public_key(), Metadata::new()); // Get room's title let title = self.title_input.read(cx).text().to_string().into(); @@ -192,7 +196,7 @@ impl Compose { _ = async_cx.update_entity(&view, |this, cx| { this.contacts.update(cx, |this, cx| { if let Some(members) = this { - members.insert(0, Contact::new(public_key, metadata)); + members.insert(0, NostrProfile::new(public_key, metadata)); } cx.notify(); }); diff --git a/crates/app/src/views/sidebar/inbox.rs b/crates/app/src/views/sidebar/inbox.rs index 709e93e..696f2c1 100644 --- a/crates/app/src/views/sidebar/inbox.rs +++ b/crates/app/src/views/sidebar/inbox.rs @@ -1,10 +1,11 @@ use crate::views::app::{AddPanel, PanelKind}; +use chat::registry::ChatRegistry; use common::utils::message_ago; use gpui::{ div, img, percentage, prelude::FluentBuilder, px, Context, InteractiveElement, IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window, }; -use registry::chat::ChatRegistry; +use std::sync::Arc; use ui::{ dock_area::dock::DockPlacement, skeleton::Skeleton, diff --git a/crates/app/src/views/sidebar/mod.rs b/crates/app/src/views/sidebar/mod.rs index 2b76e0c..bb6220e 100644 --- a/crates/app/src/views/sidebar/mod.rs +++ b/crates/app/src/views/sidebar/mod.rs @@ -1,11 +1,11 @@ use crate::views::sidebar::inbox::Inbox; +use chat::registry::ChatRegistry; use compose::Compose; use gpui::{ div, px, AnyElement, App, AppContext, BorrowAppContext, Context, Entity, EntityId, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Window, }; -use registry::chat::ChatRegistry; use ui::{ button::{Button, ButtonRounded, ButtonVariants}, dock_area::panel::{Panel, PanelEvent}, diff --git a/crates/app/src/views/startup.rs b/crates/app/src/views/startup.rs new file mode 100644 index 0000000..b70e12d --- /dev/null +++ b/crates/app/src/views/startup.rs @@ -0,0 +1,26 @@ +use gpui::{div, svg, Context, IntoElement, ParentElement, Render, Styled, Window}; +use ui::theme::{scale::ColorScaleStep, ActiveTheme}; + +pub struct Startup {} + +impl Startup { + pub fn new(_window: &mut Window, _cx: &mut Context<'_, Self>) -> Self { + Self {} + } +} + +impl Render for Startup { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .size_full() + .flex() + .items_center() + .justify_center() + .child( + svg() + .path("brand/coop.svg") + .size_12() + .text_color(cx.theme().base.step(cx, ColorScaleStep::THREE)), + ) + } +} diff --git a/crates/app_state/Cargo.toml b/crates/app_state/Cargo.toml new file mode 100644 index 0000000..b204e3d --- /dev/null +++ b/crates/app_state/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "app_state" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +common = { path = "../common" } +state = { path = "../state" } +ui = { path = "../ui" } + +gpui.workspace = true +nostr-sdk.workspace = true diff --git a/crates/app_state/src/lib.rs b/crates/app_state/src/lib.rs new file mode 100644 index 0000000..d108990 --- /dev/null +++ b/crates/app_state/src/lib.rs @@ -0,0 +1 @@ +pub mod registry; diff --git a/crates/registry/src/app.rs b/crates/app_state/src/registry.rs similarity index 58% rename from crates/registry/src/app.rs rename to crates/app_state/src/registry.rs index 596443d..604e140 100644 --- a/crates/registry/src/app.rs +++ b/crates/app_state/src/registry.rs @@ -1,38 +1,29 @@ -use common::constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID}; -use gpui::{App, AppContext, Entity, Global, WeakEntity, Window}; +use common::{ + constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID}, + profile::NostrProfile, +}; +use gpui::{App, Entity, Global, WeakEntity}; use nostr_sdk::prelude::*; use state::get_client; use std::time::Duration; - -use crate::contact::Contact; +use ui::Root; pub struct AppRegistry { - user: Entity>, - pub is_loading: bool, + root: WeakEntity, + user: Option, } impl Global for AppRegistry {} impl AppRegistry { - pub fn set_global(cx: &mut App) { - let user: Entity> = cx.new(|_| None); - let is_loading = true; - - cx.observe(&user, |this, cx| { - if let Some(contact) = this.read(cx).as_ref() { + pub fn set_global(root: WeakEntity, cx: &mut App) { + cx.observe_global::(|cx| { + if let Some(profile) = cx.global::().user() { let client = get_client(); - let public_key = contact.public_key(); + let public_key = profile.public_key(); cx.background_executor() .spawn(async move { - let subscription = Filter::new() - .kind(Kind::Metadata) - .author(public_key) - .limit(1); - - // Get metadata - _ = client.sync(subscription, &SyncOptions::default()).await; - let subscription = Filter::new() .kind(Kind::ContactList) .author(public_key) @@ -73,30 +64,18 @@ impl AppRegistry { }) .detach(); - cx.set_global(Self { user, is_loading }); + cx.set_global(Self { root, user: None }); } - pub fn user(&self) -> WeakEntity> { - self.user.downgrade() + pub fn set_user(&mut self, profile: Option) { + self.user = profile; } - pub fn current_user(&self, _window: &Window, cx: &App) -> Option { - self.user.read(cx).clone() + pub fn user(&self) -> Option { + self.user.clone() } - pub fn set_user(&mut self, contact: Contact, cx: &mut App) { - self.user.update(cx, |this, cx| { - *this = Some(contact); - cx.notify(); - }); - - self.is_loading = false; - } - - pub fn logout(&mut self, cx: &mut App) { - self.user.update(cx, |this, cx| { - *this = None; - cx.notify(); - }); + pub fn root(&self) -> Option> { + self.root.upgrade() } } diff --git a/crates/registry/Cargo.toml b/crates/chat/Cargo.toml similarity index 86% rename from crates/registry/Cargo.toml rename to crates/chat/Cargo.toml index 1a183f5..f49c650 100644 --- a/crates/registry/Cargo.toml +++ b/crates/chat/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "registry" +name = "chat" version = "0.1.0" edition = "2021" publish = false @@ -12,3 +12,4 @@ gpui.workspace = true nostr-sdk.workspace = true anyhow.workspace = true itertools.workspace = true +smol.workspace = true diff --git a/crates/chat/src/inbox.rs b/crates/chat/src/inbox.rs new file mode 100644 index 0000000..51ba83d --- /dev/null +++ b/crates/chat/src/inbox.rs @@ -0,0 +1,59 @@ +use common::utils::room_hash; +use gpui::{AsyncApp, Context, Entity, Task}; +use itertools::Itertools; +use nostr_sdk::prelude::*; +use state::get_client; +use std::cmp::Reverse; + +use crate::room::Room; + +pub struct Inbox { + pub rooms: Vec>, + pub is_loading: bool, +} + +impl Inbox { + pub fn new() -> Self { + Self { + rooms: vec![], + is_loading: true, + } + } + + pub fn get_room_ids(&self, cx: &Context) -> Vec { + self.rooms.iter().map(|room| room.read(cx).id).collect() + } + + pub fn load(&mut self, cx: AsyncApp) -> Task, Error>> { + cx.background_executor().spawn(async move { + let client = get_client(); + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + + let filter = Filter::new() + .kind(Kind::PrivateDirectMessage) + .author(public_key); + + // Get all DM events from database + let events = client.database().query(filter).await?; + + // Filter result + // - Get unique rooms only + // - Sorted by created_at + let result = events + .into_iter() + .filter(|ev| ev.tags.public_keys().peekable().peek().is_some()) + .unique_by(|ev| room_hash(&ev.tags)) + .sorted_by_key(|ev| Reverse(ev.created_at)) + .collect::>(); + + Ok(result) + }) + } +} + +impl Default for Inbox { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/chat/src/lib.rs b/crates/chat/src/lib.rs new file mode 100644 index 0000000..11912f2 --- /dev/null +++ b/crates/chat/src/lib.rs @@ -0,0 +1,3 @@ +pub mod inbox; +pub mod registry; +pub mod room; diff --git a/crates/registry/src/chat.rs b/crates/chat/src/registry.rs similarity index 69% rename from crates/registry/src/chat.rs rename to crates/chat/src/registry.rs index f54856d..d73638d 100644 --- a/crates/registry/src/chat.rs +++ b/crates/chat/src/registry.rs @@ -1,62 +1,10 @@ -use crate::room::Room; use anyhow::Error; use common::utils::{compare, room_hash}; -use gpui::{App, AppContext, AsyncApp, Context, Entity, Global, Task, WeakEntity}; -use itertools::Itertools; +use gpui::{App, AppContext, Entity, Global, WeakEntity}; use nostr_sdk::prelude::*; use state::get_client; -use std::cmp::Reverse; -pub struct Inbox { - pub rooms: Vec>, - pub is_loading: bool, -} - -impl Inbox { - pub fn new() -> Self { - Self { - rooms: vec![], - is_loading: true, - } - } - - pub fn current_rooms(&self, cx: &Context) -> Vec { - self.rooms.iter().map(|room| room.read(cx).id).collect() - } - - pub fn load(&mut self, cx: AsyncApp) -> Task, Error>> { - cx.background_executor().spawn(async move { - let client = get_client(); - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; - - let filter = Filter::new() - .kind(Kind::PrivateDirectMessage) - .author(public_key); - - // Get all DM events from database - let events = client.database().query(filter).await?; - - // Filter result - // - Get unique rooms only - // - Sorted by created_at - let result = events - .into_iter() - .filter(|ev| ev.tags.public_keys().peekable().peek().is_some()) - .unique_by(|ev| room_hash(&ev.tags)) - .sorted_by_key(|ev| Reverse(ev.created_at)) - .collect::>(); - - Ok(result) - }) - } -} - -impl Default for Inbox { - fn default() -> Self { - Self::new() - } -} +use crate::{inbox::Inbox, room::Room}; pub struct ChatRegistry { inbox: Entity, @@ -70,7 +18,7 @@ impl ChatRegistry { cx.observe_new::(|this, _window, cx| { // Get all pubkeys to load metadata - let pubkeys = this.get_all_keys(); + let pubkeys = this.get_pubkeys(); cx.spawn(|weak_model, mut async_cx| async move { let query: Result, Error> = async_cx @@ -116,7 +64,7 @@ impl ChatRegistry { if let Some(inbox) = this.upgrade() { if let Ok(events) = task.await { _ = async_cx.update_entity(&inbox, |this, cx| { - let current_rooms = this.current_rooms(cx); + let current_rooms = this.get_room_ids(cx); let items: Vec> = events .into_iter() .filter_map(|ev| { @@ -146,7 +94,7 @@ impl ChatRegistry { self.inbox.downgrade() } - pub fn room(&self, id: &u64, cx: &App) -> Option> { + pub fn get_room(&self, id: &u64, cx: &App) -> Option> { self.inbox .read(cx) .rooms @@ -166,13 +114,13 @@ impl ChatRegistry { }) } - pub fn receive(&mut self, event: Event, cx: &mut App) { + pub fn new_room_message(&mut self, event: Event, cx: &mut App) { let mut pubkeys: Vec<_> = event.tags.public_keys().copied().collect(); pubkeys.push(event.pubkey); self.inbox.update(cx, |this, cx| { if let Some(room) = this.rooms.iter().find(|room| { - let all_keys = room.read(cx).get_all_keys(); + let all_keys = room.read(cx).get_pubkeys(); compare(&all_keys, &pubkeys) }) { room.update(cx, |this, cx| { @@ -188,7 +136,7 @@ impl ChatRegistry { }) } - // cx.notify(); + cx.notify(); }) } } diff --git a/crates/registry/src/room.rs b/crates/chat/src/room.rs similarity index 83% rename from crates/registry/src/room.rs rename to crates/chat/src/room.rs index 34f387b..bf44271 100644 --- a/crates/registry/src/room.rs +++ b/crates/chat/src/room.rs @@ -1,15 +1,16 @@ -use common::utils::{compare, random_name, room_hash}; +use common::{ + profile::NostrProfile, + utils::{compare, random_name, room_hash}, +}; use gpui::SharedString; use nostr_sdk::prelude::*; -use crate::contact::Contact; - #[derive(Debug)] pub struct Room { pub id: u64, pub title: Option, - pub owner: Contact, // Owner always match current user - pub members: Vec, // Extract from event's tags + pub owner: NostrProfile, // Owner always match current user + pub members: Vec, // Extract from event's tags pub last_seen: Timestamp, pub is_group: bool, pub new_messages: Vec, // Hold all new messages @@ -30,8 +31,8 @@ impl PartialEq for Room { impl Room { pub fn new( id: u64, - owner: Contact, - members: Vec, + owner: NostrProfile, + members: Vec, title: Option, last_seen: Timestamp, ) -> Self { @@ -58,12 +59,12 @@ impl Room { let id = room_hash(&event.tags); let last_seen = event.created_at; - let owner = Contact::new(event.pubkey, Metadata::default()); - let members: Vec = event + let owner = NostrProfile::new(event.pubkey, Metadata::default()); + let members: Vec = event .tags .public_keys() .copied() - .map(|public_key| Contact::new(public_key, Metadata::default())) + .map(|public_key| NostrProfile::new(public_key, Metadata::default())) .collect(); let title = if let Some(tag) = event.tags.find(TagKind::Title) { @@ -89,7 +90,7 @@ impl Room { } /// Get room's member by public key - pub fn member(&self, public_key: &PublicKey) -> Option { + pub fn member(&self, public_key: &PublicKey) -> Option { if &self.owner.public_key() == public_key { Some(self.owner.clone()) } else { @@ -122,7 +123,7 @@ impl Room { } /// Get all public keys from room's contacts - pub fn get_all_keys(&self) -> Vec { + pub fn get_pubkeys(&self) -> Vec { let mut pubkeys: Vec<_> = self.members.iter().map(|m| m.public_key()).collect(); pubkeys.push(self.owner.public_key()); diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 49f6ff0..4a6582b 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -1,2 +1,3 @@ pub mod constants; +pub mod profile; pub mod utils; diff --git a/crates/registry/src/contact.rs b/crates/common/src/profile.rs similarity index 86% rename from crates/registry/src/contact.rs rename to crates/common/src/profile.rs index 0cfa048..3f98de8 100644 --- a/crates/registry/src/contact.rs +++ b/crates/common/src/profile.rs @@ -1,19 +1,19 @@ -use common::{constants::IMAGE_SERVICE, utils::shorted_public_key}; +use crate::{constants::IMAGE_SERVICE, utils::shorted_public_key}; use nostr_sdk::prelude::*; #[derive(Debug, Clone)] -pub struct Contact { +pub struct NostrProfile { public_key: PublicKey, metadata: Metadata, } -impl PartialEq for Contact { +impl PartialEq for NostrProfile { fn eq(&self, other: &Self) -> bool { self.public_key() == other.public_key() } } -impl Contact { +impl NostrProfile { pub fn new(public_key: PublicKey, metadata: Metadata) -> Self { Self { public_key, @@ -43,8 +43,7 @@ impl Contact { } } - /// Get contact's name - /// Fallback to public key as shorted format + /// Get contact's name, fallback to public key as shorted format pub fn name(&self) -> String { if let Some(display_name) = &self.metadata.display_name { if !display_name.is_empty() { diff --git a/crates/registry/src/lib.rs b/crates/registry/src/lib.rs deleted file mode 100644 index 9e95f98..0000000 --- a/crates/registry/src/lib.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod app; -pub mod chat; -pub mod contact; -pub mod room; diff --git a/crates/ui/src/root.rs b/crates/ui/src/root.rs index f386bd1..c4083ab 100644 --- a/crates/ui/src/root.rs +++ b/crates/ui/src/root.rs @@ -16,6 +16,7 @@ pub trait ContextModal: Sized { fn open_modal(&mut self, cx: &mut App, build: F) where F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static; + /// Return true, if there is an active Modal. fn has_active_modal(&mut self, cx: &mut App) -> bool; @@ -30,6 +31,8 @@ pub trait ContextModal: Sized { /// Pushes a notification to the notification list. fn push_notification(&mut self, note: impl Into, cx: &mut App); + + /// Clear all notifications fn clear_notifications(&mut self, cx: &mut App); } @@ -52,6 +55,7 @@ impl ContextModal for Window { focus_handle, builder: Rc::new(build), }); + cx.notify(); }) } @@ -106,62 +110,13 @@ impl ContextModal for Window { } } -// impl ContextModal for Context<'_, V> { -// fn open_drawer(&mut self, cx: &mut App, build: F) -// where -// F: Fn(Drawer, &mut Window, &mut App) -> Drawer + 'static, -// { -// self.deref_mut().open_drawer(cx, build) -// } +type Builder = Rc Modal + 'static>; -// fn open_drawer_at(&mut self, cx: &mut App, placement: Placement, build: F) -// where -// F: Fn(Drawer, &mut Window, &mut App) -> Drawer + 'static, -// { -// self.deref_mut().open_drawer_at(cx, placement, build) -// } - -// fn has_active_modal(&self, cx: &mut App) -> bool { -// self.deref().has_active_modal(cx) -// } - -// fn close_drawer(&mut self, cx: &mut App) { -// self.deref_mut().close_drawer(cx) -// } - -// fn open_modal(&mut self, cx: &mut App, build: F) -// where -// F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static, -// { -// self.deref_mut().open_modal(cx, build) -// } - -// fn has_active_drawer(&self, cx: &mut App) -> bool { -// self.deref().has_active_drawer(cx) -// } - -// /// Close the last active modal. -// fn close_modal(&mut self, cx: &mut App) { -// self.deref_mut().close_modal(cx) -// } - -// /// Close all modals. -// fn close_all_modals(&mut self, cx: &mut App) { -// self.deref_mut().close_all_modals(cx) -// } - -// fn push_notification(&mut self, cx: &mut App, note: impl Into) { -// self.deref_mut().push_notification(cx, note) -// } - -// fn clear_notifications(&mut self, cx: &mut App) { -// self.deref_mut().clear_notifications(cx) -// } - -// fn notifications(&self, cx: &mut App) -> Rc>> { -// self.deref().notifications(cx) -// } -// } +#[derive(Clone)] +struct ActiveModal { + focus_handle: FocusHandle, + builder: Builder, +} /// Root is a view for the App window for as the top level view (Must be the first view in the window). /// @@ -175,12 +130,6 @@ pub struct Root { view: AnyView, } -#[derive(Clone)] -struct ActiveModal { - focus_handle: FocusHandle, - builder: Rc Modal + 'static>, -} - impl Root { pub fn new(view: AnyView, window: &mut Window, cx: &mut Context) -> Self { Self { @@ -263,6 +212,12 @@ impl Root { pub fn view(&self) -> &AnyView { &self.view } + + /// Set the root view of the Root. + pub fn set_view(&mut self, view: AnyView, cx: &mut Context) { + self.view = view; + cx.notify(); + } } impl Render for Root {