diff --git a/.DS_Store b/.DS_Store index 13bff6e..ab05a38 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/Cargo.lock b/Cargo.lock index 7e0a686..966b2e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1094,6 +1094,7 @@ dependencies = [ "chrono", "coop-ui", "dirs 5.0.1", + "flume", "gpui", "itertools 0.13.0", "keyring", diff --git a/assets/.DS_Store b/assets/.DS_Store index 7d1d964..ced25e0 100644 Binary files a/assets/.DS_Store and b/assets/.DS_Store differ diff --git a/assets/icons/arrow-up-circle.svg b/assets/icons/arrow-up-circle.svg new file mode 100644 index 0000000..a098b52 --- /dev/null +++ b/assets/icons/arrow-up-circle.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/circle-x.svg b/assets/icons/circle-x.svg new file mode 100644 index 0000000..2a53f1d --- /dev/null +++ b/assets/icons/circle-x.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/moon.svg b/assets/icons/moon.svg new file mode 100644 index 0000000..c161221 --- /dev/null +++ b/assets/icons/moon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/plus.svg b/assets/icons/plus.svg new file mode 100644 index 0000000..8544f9b --- /dev/null +++ b/assets/icons/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/sun.svg b/assets/icons/sun.svg new file mode 100644 index 0000000..de6bb8b --- /dev/null +++ b/assets/icons/sun.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/upload.svg b/assets/icons/upload.svg new file mode 100644 index 0000000..c436e83 --- /dev/null +++ b/assets/icons/upload.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index f9a2c6a..3f3db17 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -28,3 +28,4 @@ rust-embed.workspace = true smol.workspace = true tracing-subscriber = { version = "0.3.18", features = ["fmt"] } +flume = "0.11.1" diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index 38bb4c2..48a86cb 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -10,13 +10,14 @@ use std::{ sync::{Arc, OnceLock}, time::Duration, }; -use tokio::{ - sync::{broadcast, mpsc}, - time::sleep, -}; +use tokio::{sync::mpsc, time::sleep}; use constants::{ALL_MESSAGES_SUB_ID, APP_NAME, FAKE_SIG, METADATA_DELAY, NEW_MESSAGE_SUB_ID}; -use states::{account::AccountRegistry, chat::ChatRegistry, signal::SignalRegistry}; +use states::{ + account::AccountRegistry, + chat::ChatRegistry, + metadata::{MetadataRegistry, Signal}, +}; use views::app::AppView; pub mod asset; @@ -74,23 +75,24 @@ async fn main() { // Connect to all relays _ = client.connect().await; - // Channel for metadata signal - let (signal_tx, mut signal_rx) = mpsc::channel::(1000); // TODO: adjust? + // Channel for EOSE + // When receive EOSE from relay(s) -> Load all rooms and push it into UI. + let (eose_tx, mut eose_rx) = mpsc::channel::(200); - // Channel for new chat - let (new_chat_tx, mut new_chat_rx) = mpsc::channel::(1000); // TODO: adjust? + // Channel for new message + // Push new message to chat panel or create new chat room if not exist. + let (message_tx, message_rx) = flume::unbounded::(); + let message_rx_clone = message_rx.clone(); - // Channel for all chats - // When receive EOSE from relay(s). Reload UI - let (all_chats_tx, mut all_chats_rx) = mpsc::channel::(1); - - // Channel for metadata request queue - let (queue_tx, mut queue_rx) = broadcast::channel::(100); + // Channel for signal + // Merge all metadata requests into single one. + // Notify to reload element if receive new metadata. + let (signal_tx, mut signal_rx) = mpsc::channel::(5000); + let signal_tx_clone = signal_tx.clone(); tokio::spawn(async move { let sig = Signature::from_str(FAKE_SIG).unwrap(); - let all_messages_sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID); - let new_message_sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID); + let new_message = SubscriptionId::new(NEW_MESSAGE_SUB_ID); while let Ok(notification) = notifications.recv().await { #[allow(clippy::collapsible_match)] @@ -121,69 +123,42 @@ async fn main() { ); // Save rumor to database to further query - _ = client.database().save_event(&ev).await; + if let Err(e) = client.database().save_event(&ev).await { + println!("Save error: {}", e); + } // Send event back to channel - if subscription_id == new_message_sub_id { - if let Err(e) = new_chat_tx.send(ev).await { + if subscription_id == new_message { + if let Err(e) = message_tx.send_async(ev).await { println!("Error: {}", e) } } } } } else if event.kind == Kind::Metadata { - _ = signal_tx.send(event.pubkey).await; + if let Err(e) = signal_tx.send(Signal::DONE(event.pubkey)).await { + println!("Error: {}", e) + } } } else if let RelayMessage::EndOfStoredEvents(subscription_id) = message { - if all_messages_sub_id == subscription_id { - _ = all_chats_tx.send(1).await; + if let Err(e) = eose_tx.send(subscription_id).await { + println!("Error: {}", e) } } } } }); - tokio::spawn(async move { - let mut queue: HashSet = HashSet::new(); - - while let Ok(public_key) = queue_rx.recv().await { - queue.insert(public_key); - - // Wait for METADATA_DELAY - sleep(Duration::from_millis(METADATA_DELAY)).await; - - if !queue.is_empty() { - let authors: Vec = queue.iter().copied().collect(); - let total = authors.len(); - - let filter = Filter::new() - .authors(authors) - .kind(Kind::Metadata) - .limit(total); - - let opts = SubscribeAutoCloseOptions::default() - .filter(FilterOptions::WaitDurationAfterEOSE(Duration::from_secs(2))); - - // Clear queue - queue.clear(); - - if let Err(e) = client.subscribe(vec![filter], Some(opts)).await { - println!("Error: {}", e); - } - } - } - }); - App::new() .with_assets(Assets) .with_http_client(Arc::new(reqwest_client::ReqwestClient::new())) .run(move |cx| { // Account state AccountRegistry::set_global(cx); + // Metadata state + MetadataRegistry::set_global(cx, signal_tx_clone); // Chat state - ChatRegistry::set_global(cx); - // Hold all metadata requests and merged it - SignalRegistry::set_global(cx, Arc::new(queue_tx)); + ChatRegistry::set_global(cx, message_rx); // Initialize components coop_ui::init(cx); @@ -192,16 +167,55 @@ async fn main() { cx.on_action(quit); cx.spawn(|async_cx| async move { - while let Some(public_key) = signal_rx.recv().await { - _ = async_cx.update_global::(|state, _cx| { - state.push(public_key); - }); + let mut queue: HashSet = HashSet::new(); + + while let Some(signal) = signal_rx.recv().await { + match signal { + Signal::REQ(public_key) => { + queue.insert(public_key); + + // Wait for METADATA_DELAY + sleep(Duration::from_millis(METADATA_DELAY)).await; + + if !queue.is_empty() { + let authors: Vec = queue.iter().copied().collect(); + let total = authors.len(); + + let filter = Filter::new() + .authors(authors) + .kind(Kind::Metadata) + .limit(total); + + let opts = SubscribeAutoCloseOptions::default().filter( + FilterOptions::WaitDurationAfterEOSE(Duration::from_secs(2)), + ); + + queue.clear(); + + async_cx + .background_executor() + .spawn(async move { + if let Err(e) = + client.subscribe(vec![filter], Some(opts)).await + { + println!("Error: {}", e); + } + }) + .await; + } + } + Signal::DONE(public_key) => { + _ = async_cx.update_global::(|state, _| { + state.seen(public_key); + }); + } + } } }) .detach(); cx.spawn(|async_cx| async move { - while let Some(event) = new_chat_rx.recv().await { + while let Ok(event) = message_rx_clone.recv_async().await { _ = async_cx.update_global::(|state, cx| { state.push(event, cx); }); @@ -210,10 +224,14 @@ async fn main() { .detach(); cx.spawn(|async_cx| async move { - while let Some(_n) = all_chats_rx.recv().await { - _ = async_cx.update_global::(|state, cx| { - state.load(cx); - }); + let all_messages = SubscriptionId::new(ALL_MESSAGES_SUB_ID); + + while let Some(subscription_id) = eose_rx.recv().await { + if subscription_id == all_messages { + _ = async_cx.update_global::(|state, cx| { + state.load(cx); + }); + } } }) .detach(); diff --git a/crates/app/src/states/account.rs b/crates/app/src/states/account.rs index 0419e9c..4013430 100644 --- a/crates/app/src/states/account.rs +++ b/crates/app/src/states/account.rs @@ -58,6 +58,10 @@ impl AccountRegistry { .detach(); } + pub fn get(&self) -> Option { + self.public_key + } + pub fn set_user(&mut self, public_key: Option) { self.public_key = public_key } diff --git a/crates/app/src/states/chat.rs b/crates/app/src/states/chat.rs index 8b02f35..d8f3614 100644 --- a/crates/app/src/states/chat.rs +++ b/crates/app/src/states/chat.rs @@ -1,75 +1,92 @@ +use flume::Receiver; use gpui::*; use itertools::Itertools; use nostr_sdk::prelude::*; -use std::cmp::Reverse; use crate::get_client; pub struct ChatRegistry { - events: Model>>, + chats: Model>>, + is_initialized: bool, + // Use for receive new message + pub(crate) receiver: Receiver, } impl Global for ChatRegistry {} impl ChatRegistry { - pub fn set_global(cx: &mut AppContext) { - let events = cx.new_model(|_| None); + pub fn set_global(cx: &mut AppContext, receiver: Receiver) { + let chats = cx.new_model(|_| None); - cx.set_global(Self::new(events)); + cx.set_global(Self::new(chats, receiver)); } - pub fn load(&self, cx: &mut AppContext) { + pub fn load(&mut self, cx: &mut AppContext) { let mut async_cx = cx.to_async(); - let async_events = self.events.clone(); + let async_chats = self.chats.clone(); - cx.foreground_executor() - .spawn(async move { - let client = get_client(); - let signer = client.signer().await.unwrap(); - let public_key = signer.get_public_key().await.unwrap(); + if !self.is_initialized { + self.is_initialized = true; - let filter = Filter::new() - .kind(Kind::PrivateDirectMessage) - .pubkey(public_key); + cx.foreground_executor() + .spawn(async move { + let client = get_client(); + let signer = client.signer().await.unwrap(); + let public_key = signer.get_public_key().await.unwrap(); - let events = async_cx - .background_executor() - .spawn(async move { - if let Ok(events) = client.database().query(vec![filter]).await { - events - .into_iter() - .filter(|ev| ev.pubkey != public_key) // Filter messages from current user - .unique_by(|ev| ev.pubkey) // Get unique list - .sorted_by_key(|ev| Reverse(ev.created_at)) // Sort by created at - .collect::>() - } else { - Vec::new() - } + let filter = Filter::new() + .kind(Kind::PrivateDirectMessage) + .pubkey(public_key); + + let events = async_cx + .background_executor() + .spawn(async move { + if let Ok(events) = client.database().query(vec![filter]).await { + events + .into_iter() + .filter(|ev| ev.pubkey != public_key) // Filter all messages from current user + .unique_by(|ev| ev.pubkey) // Get unique list + .collect::>() + } else { + Vec::new() + } + }) + .await; + + async_cx.update_model(&async_chats, |a, b| { + *a = Some(events); + b.notify(); }) - .await; - - async_cx.update_model(&async_events, |a, b| { - *a = Some(events); - b.notify(); }) - }) - .detach(); + .detach(); + } } pub fn push(&self, event: Event, cx: &mut AppContext) { - cx.update_model(&self.events, |a, b| { - if let Some(events) = a { - events.push(event); - b.notify(); + cx.update_model(&self.chats, |a, b| { + if let Some(chats) = a { + if let Some(index) = chats.iter().position(|c| c.pubkey == event.pubkey) { + chats.swap_remove(index); + chats.push(event); + + b.notify(); + } else { + chats.push(event); + b.notify(); + } } }) } pub fn get(&self, cx: &WindowContext) -> Option> { - self.events.read(cx).clone() + self.chats.read(cx).clone() } - fn new(events: Model>>) -> Self { - Self { events } + fn new(chats: Model>>, receiver: Receiver) -> Self { + Self { + chats, + receiver, + is_initialized: false, + } } } diff --git a/crates/app/src/states/metadata.rs b/crates/app/src/states/metadata.rs new file mode 100644 index 0000000..880e968 --- /dev/null +++ b/crates/app/src/states/metadata.rs @@ -0,0 +1,41 @@ +use gpui::*; +use nostr_sdk::prelude::*; +use tokio::sync::mpsc::Sender; + +#[derive(Clone)] +pub enum Signal { + /// Send + DONE(PublicKey), + /// Receive + REQ(PublicKey), +} + +pub struct MetadataRegistry { + seens: Vec, + pub reqs: Sender, +} + +impl Global for MetadataRegistry {} + +impl MetadataRegistry { + pub fn set_global(cx: &mut AppContext, reqs: Sender) { + cx.set_global(Self::new(reqs)); + } + + pub fn contains(&self, public_key: PublicKey) -> bool { + self.seens.contains(&public_key) + } + + pub fn seen(&mut self, public_key: PublicKey) { + if !self.seens.contains(&public_key) { + self.seens.push(public_key); + } + } + + fn new(reqs: Sender) -> Self { + Self { + seens: Vec::new(), + reqs, + } + } +} diff --git a/crates/app/src/states/mod.rs b/crates/app/src/states/mod.rs index b4ed23c..0cd8521 100644 --- a/crates/app/src/states/mod.rs +++ b/crates/app/src/states/mod.rs @@ -1,3 +1,3 @@ pub mod account; pub mod chat; -pub mod signal; +pub mod metadata; diff --git a/crates/app/src/states/signal.rs b/crates/app/src/states/signal.rs deleted file mode 100644 index ced90fc..0000000 --- a/crates/app/src/states/signal.rs +++ /dev/null @@ -1,38 +0,0 @@ -use gpui::*; -use nostr_sdk::prelude::*; -use std::sync::Arc; -use tokio::sync::broadcast::Sender; - -pub struct SignalRegistry { - public_keys: Vec, - pub queue: Arc>, -} - -impl Global for SignalRegistry {} - -impl SignalRegistry { - pub fn set_global(cx: &mut AppContext, queue: Arc>) { - cx.set_global(Self::new(queue)); - } - - pub fn contains(&self, public_key: PublicKey) -> bool { - self.public_keys.contains(&public_key) - } - - pub fn push(&mut self, public_key: PublicKey) { - self.public_keys.push(public_key); - } - - pub fn add_to_queue(&mut self, public_key: PublicKey) { - if let Err(e) = self.queue.send(public_key) { - println!("Dropped: {}", e) - } - } - - fn new(queue: Arc>) -> Self { - Self { - public_keys: Vec::new(), - queue, - } - } -} diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index 81a7e63..34c70e7 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -41,7 +41,7 @@ pub fn ago(time: u64) -> String { "now".to_owned() } else if diff < 24 { let duration = now.signed_duration_since(input_time); - format!("{} ago", duration.num_hours()) + format!("{} hours ago", duration.num_hours()) } else { input_time.format("%b %d").to_string() } diff --git a/crates/app/src/views/app.rs b/crates/app/src/views/app.rs index c7b2b71..f1d0fa3 100644 --- a/crates/app/src/views/app.rs +++ b/crates/app/src/views/app.rs @@ -1,10 +1,12 @@ use coop_ui::{ + button::{Button, ButtonVariants}, dock::{DockArea, DockItem, DockPlacement}, - theme::{ActiveTheme, Theme}, - Root, TitleBar, + theme::{ActiveTheme, Theme, ThemeMode}, + IconName, Root, Sizable, TitleBar, }; use gpui::*; use nostr_sdk::prelude::*; +use prelude::FluentBuilder; use serde::Deserialize; use std::sync::Arc; @@ -62,6 +64,18 @@ impl AppView { AppView { onboarding, dock } } + fn change_theme_mode(&mut self, _: &ClickEvent, cx: &mut ViewContext) { + let mode = match cx.theme().mode.is_dark() { + true => ThemeMode::Light, + false => ThemeMode::Dark, + }; + + // Change theme + Theme::change(mode, cx); + // Rerender + cx.refresh(); + } + fn init_layout(dock_area: WeakView, cx: &mut WindowContext) { let left = DockItem::panel(Arc::new(LeftDock::new(cx))); let center = Self::init_dock_items(&dock_area, cx); @@ -119,7 +133,33 @@ impl Render for AppView { .size_full() .flex() .flex_col() - .child(TitleBar::new()) + .child( + TitleBar::new() + // Left side + .child(div()) + // Right side + .child( + div() + .flex() + .items_center() + .justify_end() + .px_2() + .gap_2() + .child( + Button::new("theme-mode") + .map(|this| { + if cx.theme().mode.is_dark() { + this.icon(IconName::Sun) + } else { + this.icon(IconName::Moon) + } + }) + .small() + .ghost() + .on_click(cx.listener(Self::change_theme_mode)), + ), + ), + ) .child(self.dock.clone()) } else { content = content.size_full().child(self.onboarding.clone()) diff --git a/crates/app/src/views/dock/chat/form.rs b/crates/app/src/views/dock/chat/form.rs new file mode 100644 index 0000000..f0cc5cc --- /dev/null +++ b/crates/app/src/views/dock/chat/form.rs @@ -0,0 +1,91 @@ +use coop_ui::{ + button::{Button, ButtonVariants}, + input::{InputEvent, TextInput}, + theme::ActiveTheme, + Icon, IconName, +}; +use gpui::*; +use nostr_sdk::prelude::*; + +use crate::get_client; + +pub struct Form { + to: PublicKey, + input: View, +} + +impl Form { + pub fn new(to: PublicKey, cx: &mut ViewContext<'_, Self>) -> Self { + let input = cx.new_view(|cx| { + TextInput::new(cx) + .appearance(false) + .text_size(coop_ui::Size::Small) + .placeholder("Message...") + .cleanable() + }); + + cx.subscribe(&input, move |form, text_input, input_event, cx| { + if let InputEvent::PressEnter = input_event { + let content = text_input.read(cx).text().to_string(); + // TODO: clean up content + + form.send_message(content, cx); + } + }) + .detach(); + + Self { to, input } + } + + fn send_message(&mut self, content: String, cx: &mut ViewContext) { + let send_to = self.to; + let content_clone = content.clone(); + + cx.foreground_executor() + .spawn(async move { + let client = get_client(); + let signer = client.signer().await.unwrap(); + let public_key = signer.get_public_key().await.unwrap(); + + match client.send_private_msg(send_to, content, vec![]).await { + Ok(_) => { + // Send a copy to yourself + if let Err(_e) = client + .send_private_msg(public_key, content_clone, vec![]) + .await + { + todo!() + } + } + Err(_) => todo!(), + } + }) + .detach(); + } +} + +impl Render for Form { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div() + .h_12() + .flex_shrink_0() + .flex() + .items_center() + .gap_2() + .px_2() + .child( + Button::new("upload") + .icon(Icon::new(IconName::Upload)) + .ghost(), + ) + .child( + div() + .flex_1() + .flex() + .bg(cx.theme().muted) + .rounded(px(cx.theme().radius)) + .px_2() + .child(self.input.clone()), + ) + } +} diff --git a/crates/app/src/views/dock/chat/messages.rs b/crates/app/src/views/dock/chat/list.rs similarity index 56% rename from crates/app/src/views/dock/chat/messages.rs rename to crates/app/src/views/dock/chat/list.rs index 56a75ce..6cce8f0 100644 --- a/crates/app/src/views/dock/chat/messages.rs +++ b/crates/app/src/views/dock/chat/list.rs @@ -1,16 +1,26 @@ use gpui::*; use nostr_sdk::prelude::*; -use crate::get_client; +use crate::{get_client, states::chat::ChatRegistry}; -pub struct Messages { +pub struct MessageList { + member: PublicKey, messages: Model>, } -impl Messages { +impl MessageList { pub fn new(from: PublicKey, cx: &mut ViewContext<'_, Self>) -> Self { let messages = cx.new_model(|_| None); - let async_messages = messages.clone(); + + Self { + member: from, + messages, + } + } + + pub fn init(&self, cx: &mut ViewContext) { + let messages = self.messages.clone(); + let member = self.member; let mut async_cx = cx.to_async(); @@ -20,40 +30,45 @@ impl Messages { let signer = client.signer().await.unwrap(); let public_key = signer.get_public_key().await.unwrap(); - let recv_filter = Filter::new() + let recv = Filter::new() .kind(Kind::PrivateDirectMessage) - .author(from) + .author(member) .pubkey(public_key); - let sender_filter = Filter::new() + let send = Filter::new() .kind(Kind::PrivateDirectMessage) .author(public_key) - .pubkey(from); + .pubkey(member); let events = async_cx .background_executor() - .spawn(async move { - client - .database() - .query(vec![recv_filter, sender_filter]) - .await - }) + .spawn(async move { client.database().query(vec![recv, send]).await }) .await; if let Ok(events) = events { - _ = async_cx.update_model(&async_messages, |a, b| { + _ = async_cx.update_model(&messages, |a, b| { *a = Some(events); b.notify(); }); } }) .detach(); + } - Self { messages } + pub fn subscribe(&self, cx: &mut ViewContext) { + let receiver = cx.global::().receiver.clone(); + + cx.foreground_executor() + .spawn(async move { + while let Ok(event) = receiver.recv_async().await { + println!("New message: {}", event.as_json()) + } + }) + .detach(); } } -impl Render for Messages { +impl Render for MessageList { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let mut content = div().size_full().flex().flex_col().justify_end(); diff --git a/crates/app/src/views/dock/chat/mod.rs b/crates/app/src/views/dock/chat/mod.rs index fc0203b..e7addd8 100644 --- a/crates/app/src/views/dock/chat/mod.rs +++ b/crates/app/src/views/dock/chat/mod.rs @@ -1,16 +1,15 @@ use coop_ui::{ button::Button, - button_group::ButtonGroup, dock::{DockItemState, Panel, PanelEvent, TitleStyle}, - input::TextInput, popup_menu::PopupMenu, - Sizable, }; +use form::Form; use gpui::*; -use messages::Messages; +use list::MessageList; use nostr_sdk::*; -pub mod messages; +pub mod form; +pub mod list; pub struct ChatPanel { // Panel @@ -19,17 +18,21 @@ pub struct ChatPanel { zoomable: bool, focus_handle: FocusHandle, // Chat Room - messages: View, - input: View, + list: View, + form: View
, } impl ChatPanel { pub fn new(from: PublicKey, cx: &mut WindowContext) -> View { - let input = cx.new_view(TextInput::new); - let messages = cx.new_view(|cx| Messages::new(from, cx)); + let form = cx.new_view(|cx| Form::new(from, cx)); + let list = cx.new_view(|cx| { + let list = MessageList::new(from, cx); + // Load messages from database + list.init(cx); + // Subscribe for new message + list.subscribe(cx); - input.update(cx, |input, _cx| { - input.set_placeholder("Message"); + list }); cx.new_view(|cx| Self { @@ -37,8 +40,8 @@ impl ChatPanel { closeable: true, zoomable: true, focus_handle: cx.focus_handle(), - messages, - input, + list, + form, }) } } @@ -91,22 +94,7 @@ impl Render for ChatPanel { .size_full() .flex() .flex_col() - .child(self.messages.clone()) - .child( - div() - .flex_shrink_0() - .flex() - .items_center() - .gap_2() - .px_2() - .h_11() - .child(self.input.clone()) - .child( - ButtonGroup::new("actions") - .large() - .child(Button::new("upload").label("Upload")) - .child(Button::new("send").label("Send")), - ), - ) + .child(self.list.clone()) + .child(self.form.clone()) } } diff --git a/crates/app/src/views/dock/inbox/chat.rs b/crates/app/src/views/dock/inbox/chat.rs index 609fc40..510e20f 100644 --- a/crates/app/src/views/dock/inbox/chat.rs +++ b/crates/app/src/views/dock/inbox/chat.rs @@ -5,7 +5,7 @@ use prelude::FluentBuilder; use crate::{ get_client, - states::signal::SignalRegistry, + states::metadata::{MetadataRegistry, Signal}, utils::{ago, show_npub}, views::app::AddPanel, }; @@ -153,6 +153,55 @@ impl Chat { let mut async_cx = cx.to_async(); + let client = get_client(); + let signal = cx.global::(); + + if !signal.contains(public_key) { + cx.foreground_executor() + .spawn(async move { + 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(); + } else { + let reqs = signal.reqs.clone(); + + cx.foreground_executor() + .spawn(async move { + if let Err(e) = reqs.send(Signal::REQ(public_key)).await { + println!("Error: {}", e) + } + }) + .detach(); + + cx.observe_global::(|view, cx| { + view.profile(cx); + }) + .detach(); + }; + + Self { + public_key, + last_seen, + metadata, + title: None, + } + } + + fn profile(&self, cx: &mut ViewContext) { + let public_key = self.public_key; + let async_metadata = self.metadata.clone(); + let mut async_cx = cx.to_async(); + cx.foreground_executor() .spawn(async move { let client = get_client(); @@ -169,47 +218,6 @@ impl Chat { }; }) .detach(); - - cx.update_global::(|state, _cx| { - state.add_to_queue(public_key); - }); - - cx.observe_global::(|chat, cx| { - chat.load_profile(cx); - }) - .detach(); - - Self { - public_key, - last_seen, - metadata, - title: None, - } - } - - fn load_profile(&self, cx: &mut ViewContext) { - let public_key = self.public_key; - let async_metadata = self.metadata.clone(); - let mut async_cx = cx.to_async(); - - if cx.global::().contains(self.public_key) { - 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(); - } } } diff --git a/crates/app/src/views/dock/inbox/mod.rs b/crates/app/src/views/dock/inbox/mod.rs index 2c61f44..40f314b 100644 --- a/crates/app/src/views/dock/inbox/mod.rs +++ b/crates/app/src/views/dock/inbox/mod.rs @@ -1,10 +1,11 @@ use chat::Chat; use coop_ui::{theme::ActiveTheme, v_flex, Collapsible, Icon, IconName, StyledExt}; use gpui::*; - +use itertools::Itertools; use prelude::FluentBuilder; +use std::cmp::Reverse; -use crate::states::chat::ChatRegistry; +use crate::states::{account::AccountRegistry, chat::ChatRegistry}; pub mod chat; @@ -19,7 +20,6 @@ impl Inbox { pub fn new(cx: &mut ViewContext<'_, Self>) -> Self { let chats = cx.new_model(|_| None); - // Reload UI if global state changes cx.observe_global::(|inbox, cx| { inbox.load(cx); }) @@ -39,17 +39,22 @@ impl Inbox { // Read global chat registry let events = cx.global::().get(cx); + let current_user = cx.global::().get(); - if let Some(events) = events { - let chats: Vec> = events - .into_iter() - .map(|event| cx.new_view(|cx| Chat::new(event, cx))) - .collect(); + if let Some(public_key) = current_user { + if let Some(events) = events { + let chats: Vec> = events + .into_iter() + .filter(|ev| ev.pubkey != public_key) + .sorted_by_key(|ev| Reverse(ev.created_at)) + .map(|ev| cx.new_view(|cx| Chat::new(ev, cx))) + .collect(); - cx.update_model(&self.chats, |a, b| { - *a = Some(chats); - b.notify(); - }); + cx.update_model(&self.chats, |a, b| { + *a = Some(chats); + b.notify(); + }); + } } } diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index ab068c3..ac07ce3 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -11,6 +11,7 @@ pub enum IconName { ArrowLeft, ArrowRight, ArrowUp, + ArrowUpCircle, Asterisk, Bell, BookOpen, @@ -73,6 +74,7 @@ pub enum IconName { ThumbsDown, ThumbsUp, TriangleAlert, + Upload, WindowClose, WindowMaximize, WindowMinimize, @@ -87,6 +89,7 @@ impl IconName { IconName::ArrowLeft => "icons/arrow-left.svg", IconName::ArrowRight => "icons/arrow-right.svg", IconName::ArrowUp => "icons/arrow-up.svg", + IconName::ArrowUpCircle => "icons/arrow-up-circle.svg", IconName::Asterisk => "icons/asterisk.svg", IconName::Bell => "icons/bell.svg", IconName::BookOpen => "icons/book-open.svg", @@ -149,6 +152,7 @@ impl IconName { IconName::ThumbsDown => "icons/thumbs-down.svg", IconName::ThumbsUp => "icons/thumbs-up.svg", IconName::TriangleAlert => "icons/triangle-alert.svg", + IconName::Upload => "icons/upload.svg", IconName::WindowClose => "icons/window-close.svg", IconName::WindowMaximize => "icons/window-maximize.svg", IconName::WindowMinimize => "icons/window-minimize.svg", diff --git a/crates/ui/src/input/input.rs b/crates/ui/src/input/input.rs index c527300..17430c6 100644 --- a/crates/ui/src/input/input.rs +++ b/crates/ui/src/input/input.rs @@ -178,6 +178,7 @@ pub struct TextInput { pub(super) appearance: bool, pub(super) cleanable: bool, pub(super) size: Size, + pub(super) text_size: Size, pub(super) rows: usize, pattern: Option, validate: Validate, @@ -215,6 +216,7 @@ impl TextInput { prefix: None, suffix: None, size: Size::Medium, + text_size: Size::Medium, pattern: None, validate: None, rows: 2, @@ -333,6 +335,12 @@ impl TextInput { cx.notify(); } + /// Set the Input size + pub fn text_size(mut self, size: Size) -> Self { + self.text_size = size; + self + } + /// Set the appearance of the input field. pub fn appearance(mut self, appearance: bool) -> Self { self.appearance = appearance; @@ -1204,6 +1212,7 @@ impl Render for TextInput { .line_height(LINE_HEIGHT) .input_py(self.size) .input_h(self.size) + .input_text_size(self.text_size) .cursor_text() .when(self.multi_line, |this| { this.on_action(cx.listener(Self::up)) diff --git a/crates/ui/src/styled.rs b/crates/ui/src/styled.rs index 56b08fa..b260ce3 100644 --- a/crates/ui/src/styled.rs +++ b/crates/ui/src/styled.rs @@ -215,17 +215,22 @@ pub trait Sizable: Sized { /// Or a `Pixels` to set a custom size: `px(30.)` fn with_size(self, size: impl Into) -> Self; - /// Set to Size::Small - fn small(self) -> Self { - self.with_size(Size::Small) - } - /// Set to Size::XSmall fn xsmall(self) -> Self { self.with_size(Size::XSmall) } + /// Set to Size::Small + fn small(self) -> Self { + self.with_size(Size::Small) + } + /// Set to Size::Medium + fn medium(self) -> Self { + self.with_size(Size::Medium) + } + + /// Set to Size::Large fn large(self) -> Self { self.with_size(Size::Large) } diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index b76ae37..679e9fe 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -527,8 +527,8 @@ impl From for Theme { } else { "FreeMono".into() }, - radius: 4.0, - shadow: true, + radius: 5.0, + shadow: false, scrollbar_show: ScrollbarShow::default(), colors, }