diff --git a/assets/brand/avatar.png b/assets/brand/avatar.png new file mode 100644 index 0000000..af45456 Binary files /dev/null and b/assets/brand/avatar.png differ diff --git a/assets/icons/chevron-down.svg b/assets/icons/chevron-down.svg new file mode 100644 index 0000000..944e76e --- /dev/null +++ b/assets/icons/chevron-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/ellipsis.svg b/assets/icons/ellipsis.svg index 05eec51..f3113af 100644 --- a/assets/icons/ellipsis.svg +++ b/assets/icons/ellipsis.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/minimize.svg b/assets/icons/minimize.svg index 825fc19..67009bf 100644 --- a/assets/icons/minimize.svg +++ b/assets/icons/minimize.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/icons/panel-left-open.svg b/assets/icons/panel-left-open.svg index a4914be..6f79cd1 100644 --- a/assets/icons/panel-left-open.svg +++ b/assets/icons/panel-left-open.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/panel-left.svg b/assets/icons/panel-left.svg index a4914be..6f79cd1 100644 --- a/assets/icons/panel-left.svg +++ b/assets/icons/panel-left.svg @@ -1,4 +1,4 @@ - - + + diff --git a/crates/app/src/constants.rs b/crates/app/src/constants.rs index 625a0e5..1574c22 100644 --- a/crates/app/src/constants.rs +++ b/crates/app/src/constants.rs @@ -1,5 +1,6 @@ pub const KEYRING_SERVICE: &str = "Coop Safe Storage"; pub const APP_NAME: &str = "coop"; pub const FAKE_SIG: &str = "f9e79d141c004977192d05a86f81ec7c585179c371f7350a5412d33575a2a356433f58e405c2296ed273e2fe0aafa25b641e39cc4e1f3f261ebf55bce0cbac83"; -pub const SUBSCRIPTION_ID: &str = "listen_new_giftwrap"; +pub const NEW_MESSAGE_SUB_ID: &str = "listen_new_giftwrap"; +pub const ALL_MESSAGES_SUB_ID: &str = "listen_all_giftwraps"; pub const METADATA_DELAY: u64 = 150; diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index fb71539..eb0ea20 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -9,9 +9,10 @@ use std::{ sync::{Arc, OnceLock}, time::Duration, }; +use tokio::sync::mpsc; -use constants::{APP_NAME, FAKE_SIG}; -use states::account::AccountState; +use constants::{ALL_MESSAGES_SUB_ID, APP_NAME, FAKE_SIG, NEW_MESSAGE_SUB_ID}; +use states::{account::AccountRegistry, chat::ChatRegistry, signal::SignalRegistry}; use views::app::AppView; pub mod asset; @@ -69,11 +70,76 @@ 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 new chat + let (new_chat_tx, mut new_chat_rx) = mpsc::channel::(1000); // TODO: adjust? + + // Channel for all chats + let (all_chats_tx, mut all_chats_rx) = mpsc::channel::(1); + + 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); + + while let Ok(notification) = notifications.recv().await { + #[allow(clippy::collapsible_match)] + if let RelayPoolNotification::Message { message, .. } = notification { + if let RelayMessage::Event { + event, + subscription_id, + } = message + { + if event.kind == Kind::GiftWrap { + if let Ok(UnwrappedGift { rumor, .. }) = + client.unwrap_gift_wrap(&event).await + { + let mut rumor_clone = rumor.clone(); + + // Compute event id if not exist + rumor_clone.ensure_id(); + + if let Some(id) = rumor_clone.id { + let ev = Event::new( + id, + rumor_clone.pubkey, + rumor_clone.created_at, + rumor_clone.kind, + rumor_clone.tags, + rumor_clone.content, + sig, + ); + + // Save rumor to database to further query + _ = client.database().save_event(&ev).await; + + // Send event to channel + if subscription_id == new_message_sub_id { + _ = new_chat_tx.send(ev).await; + } + } + } + } else if event.kind == Kind::Metadata { + _ = signal_tx.send(event.pubkey).await; + } + } else if let RelayMessage::EndOfStoredEvents(subscription_id) = message { + if all_messages_sub_id == subscription_id { + _ = all_chats_tx.send(1).await; + } + } + } + } + }); + App::new() .with_assets(Assets) .with_http_client(Arc::new(reqwest_client::ReqwestClient::new())) .run(move |cx| { - AccountState::set_global(cx); + AccountRegistry::set_global(cx); + ChatRegistry::set_global(cx); + SignalRegistry::set_global(cx); // Initialize components coop_ui::init(cx); @@ -81,46 +147,32 @@ async fn main() { // Set quit action cx.on_action(quit); - // Handle notifications - cx.background_executor() - .spawn({ - let sig = Signature::from_str(FAKE_SIG).unwrap(); - async move { - while let Ok(notification) = notifications.recv().await { - #[allow(clippy::collapsible_match)] - if let RelayPoolNotification::Message { message, .. } = notification { - if let RelayMessage::Event { event, .. } = message { - if event.kind == Kind::GiftWrap { - if let Ok(UnwrappedGift { rumor, .. }) = - client.unwrap_gift_wrap(&event).await - { - let mut rumor_clone = rumor.clone(); + 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); + }); + } + }) + .detach(); - // Compute event id if not exist - rumor_clone.ensure_id(); + cx.spawn(|async_cx| async move { + while let Some(event) = new_chat_rx.recv().await { + _ = async_cx.update_global::(|state, cx| { + state.push(event, cx); + }); + } + }) + .detach(); - if let Some(id) = rumor_clone.id { - let ev = Event::new( - id, - rumor_clone.pubkey, - rumor_clone.created_at, - rumor_clone.kind, - rumor_clone.tags, - rumor_clone.content, - sig, - ); - - // Save rumor to database to further query - _ = client.database().save_event(&ev).await - } - } - } - } - } - } - } - }) - .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); + }); + } + }) + .detach(); // Set window size let bounds = Bounds::centered(None, size(px(900.0), px(680.0)), cx); diff --git a/crates/app/src/states/account.rs b/crates/app/src/states/account.rs index 2982355..0419e9c 100644 --- a/crates/app/src/states/account.rs +++ b/crates/app/src/states/account.rs @@ -3,33 +3,36 @@ use gpui::*; use nostr_sdk::prelude::*; use std::time::Duration; -use crate::{constants::SUBSCRIPTION_ID, get_client}; +use crate::{ + constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID}, + get_client, +}; -pub struct AccountState { - pub in_use: Option, +pub struct AccountRegistry { + public_key: Option, } -impl Global for AccountState {} +impl Global for AccountRegistry {} -impl AccountState { +impl AccountRegistry { pub fn set_global(cx: &mut AppContext) { cx.set_global(Self::new()); cx.observe_global::(|cx| { let state = cx.global::(); - if let Some(public_key) = state.in_use { + if let Some(public_key) = state.public_key { let client = get_client(); + let all_messages_sub_id = SubscriptionId::new(ALL_MESSAGES_SUB_ID); + let new_message_sub_id = SubscriptionId::new(NEW_MESSAGE_SUB_ID); + // Create a filter for getting all gift wrapped events send to current user let all_messages = Filter::new().kind(Kind::GiftWrap).pubkey(public_key); // Subscription options - let opts = SubscribeAutoCloseOptions::default().filter( - FilterOptions::WaitDurationAfterEOSE(Duration::from_secs(10)), - ); - - let subscription_id = SubscriptionId::new(SUBSCRIPTION_ID); + let opts = SubscribeAutoCloseOptions::default() + .filter(FilterOptions::WaitDurationAfterEOSE(Duration::from_secs(5))); // Create a filter for getting new message let new_message = Filter::new() @@ -38,14 +41,15 @@ impl AccountState { .limit(0); spawn(async move { + // Subscribe for all messages if client - .subscribe(vec![all_messages], Some(opts)) + .subscribe_with_id(all_messages_sub_id, vec![all_messages], Some(opts)) .await .is_ok() { // Subscribe for new message _ = client - .subscribe_with_id(subscription_id, vec![new_message], None) + .subscribe_with_id(new_message_sub_id, vec![new_message], None) .await } }); @@ -54,7 +58,15 @@ impl AccountState { .detach(); } + pub fn set_user(&mut self, public_key: Option) { + self.public_key = public_key + } + + pub fn is_user_logged_in(&self) -> bool { + self.public_key.is_some() + } + fn new() -> Self { - Self { in_use: None } + Self { public_key: None } } } diff --git a/crates/app/src/states/chat.rs b/crates/app/src/states/chat.rs new file mode 100644 index 0000000..8b02f35 --- /dev/null +++ b/crates/app/src/states/chat.rs @@ -0,0 +1,75 @@ +use gpui::*; +use itertools::Itertools; +use nostr_sdk::prelude::*; +use std::cmp::Reverse; + +use crate::get_client; + +pub struct ChatRegistry { + events: Model>>, +} + +impl Global for ChatRegistry {} + +impl ChatRegistry { + pub fn set_global(cx: &mut AppContext) { + let events = cx.new_model(|_| None); + + cx.set_global(Self::new(events)); + } + + pub fn load(&self, cx: &mut AppContext) { + let mut async_cx = cx.to_async(); + let async_events = self.events.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(); + + 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 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() + } + }) + .await; + + async_cx.update_model(&async_events, |a, b| { + *a = Some(events); + b.notify(); + }) + }) + .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(); + } + }) + } + + pub fn get(&self, cx: &WindowContext) -> Option> { + self.events.read(cx).clone() + } + + fn new(events: Model>>) -> Self { + Self { events } + } +} diff --git a/crates/app/src/states/mod.rs b/crates/app/src/states/mod.rs index b0edc6c..b4ed23c 100644 --- a/crates/app/src/states/mod.rs +++ b/crates/app/src/states/mod.rs @@ -1 +1,3 @@ pub mod account; +pub mod chat; +pub mod signal; diff --git a/crates/app/src/states/signal.rs b/crates/app/src/states/signal.rs new file mode 100644 index 0000000..b5714af --- /dev/null +++ b/crates/app/src/states/signal.rs @@ -0,0 +1,28 @@ +use gpui::*; +use nostr_sdk::prelude::*; + +pub struct SignalRegistry { + public_keys: Vec, +} + +impl Global for SignalRegistry {} + +impl SignalRegistry { + pub fn set_global(cx: &mut AppContext) { + cx.set_global(Self::new()); + } + + 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); + } + + fn new() -> Self { + Self { + public_keys: Vec::new(), + } + } +} diff --git a/crates/app/src/utils.rs b/crates/app/src/utils.rs index b7f7c41..81a7e63 100644 --- a/crates/app/src/utils.rs +++ b/crates/app/src/utils.rs @@ -15,8 +15,8 @@ pub fn get_all_accounts_from_keyring() -> Vec { accounts } -pub fn show_npub(public_key: PublicKey, len: usize) -> anyhow::Result { - let bech32 = public_key.to_bech32()?; +pub fn show_npub(public_key: PublicKey, len: usize) -> String { + let bech32 = public_key.to_bech32().unwrap_or_default(); let separator = " ... "; let sep_len = separator.len(); @@ -24,12 +24,12 @@ pub fn show_npub(public_key: PublicKey, len: usize) -> anyhow::Result String { diff --git a/crates/app/src/views/app.rs b/crates/app/src/views/app.rs index 122b617..f70aced 100644 --- a/crates/app/src/views/app.rs +++ b/crates/app/src/views/app.rs @@ -1,5 +1,5 @@ use coop_ui::{ - dock::{DockArea, DockItem, DockPlacement, PanelStyle}, + dock::{DockArea, DockItem, DockPlacement}, theme::{ActiveTheme, Theme}, Root, TitleBar, }; @@ -12,7 +12,7 @@ use super::{ dock::{chat::ChatPanel, left_dock::LeftDock, welcome::WelcomePanel}, onboarding::Onboarding, }; -use crate::states::account::AccountState; +use crate::states::account::AccountRegistry; #[derive(Clone, PartialEq, Eq, Deserialize)] pub struct AddPanel { @@ -49,13 +49,11 @@ impl AppView { let onboarding = cx.new_view(Onboarding::new); // Dock - let dock = cx.new_view(|cx| { - DockArea::new(DOCK_AREA.id, Some(DOCK_AREA.version), cx).panel_style(PanelStyle::TabBar) - }); + let dock = cx.new_view(|cx| DockArea::new(DOCK_AREA.id, Some(DOCK_AREA.version), cx)); - cx.observe_global::(|view, cx| { + cx.observe_global::(|view, cx| { // TODO: save dock state and load previous state on startup - if cx.global::().in_use.is_some() { + if cx.global::().is_user_logged_in() { Self::init_layout(view.dock.downgrade(), cx); } }) @@ -115,9 +113,7 @@ impl Render for AppView { let mut content = div(); - if cx.global::().in_use.is_none() { - content = content.size_full().child(self.onboarding.clone()) - } else { + if cx.global::().is_user_logged_in() { content = content .on_action(cx.listener(Self::on_action_add_panel)) .size_full() @@ -125,6 +121,8 @@ impl Render for AppView { .flex_col() .child(TitleBar::new()) .child(self.dock.clone()) + } else { + content = content.size_full().child(self.onboarding.clone()) } div() diff --git a/crates/app/src/views/dock/inbox/chat.rs b/crates/app/src/views/dock/inbox/chat.rs index 28e50cd..c599932 100644 --- a/crates/app/src/views/dock/inbox/chat.rs +++ b/crates/app/src/views/dock/inbox/chat.rs @@ -1,63 +1,49 @@ -use coop_ui::{theme::ActiveTheme, Collapsible, Selectable, StyledExt}; +use coop_ui::{theme::ActiveTheme, Selectable, StyledExt}; use gpui::*; use nostr_sdk::prelude::*; use prelude::FluentBuilder; -use serde::Deserialize; use crate::{ + get_client, + states::signal::SignalRegistry, utils::{ago, show_npub}, views::app::AddPanel, }; -#[derive(Clone, PartialEq, Eq, Deserialize)] -pub struct ChatDelegate { - title: Option, +#[derive(IntoElement)] +struct ChatItem { + id: ElementId, public_key: PublicKey, metadata: Option, last_seen: Timestamp, -} - -impl ChatDelegate { - pub fn new( - title: Option, - public_key: PublicKey, - metadata: Option, - last_seen: Timestamp, - ) -> Self { - Self { - title, - public_key, - metadata, - last_seen, - } - } -} - -#[derive(IntoElement)] -pub struct Chat { - id: ElementId, - pub item: ChatDelegate, + title: Option, // Interactive base: Div, selected: bool, - is_collapsed: bool, } -impl Chat { - pub fn new(item: ChatDelegate) -> Self { - let id = SharedString::from(item.public_key.to_hex()).into(); +impl ChatItem { + pub fn new( + public_key: PublicKey, + metadata: Option, + last_seen: Timestamp, + title: Option, + ) -> Self { + let id = SharedString::from(public_key.to_hex()).into(); Self { id, - item, + public_key, + metadata, + last_seen, + title, base: div(), selected: false, - is_collapsed: false, } } } -impl Selectable for Chat { +impl Selectable for ChatItem { fn selected(mut self, selected: bool) -> Self { self.selected = selected; self @@ -68,32 +54,22 @@ impl Selectable for Chat { } } -impl Collapsible for Chat { - fn is_collapsed(&self) -> bool { - self.is_collapsed - } - - fn collapsed(mut self, collapsed: bool) -> Self { - self.is_collapsed = collapsed; - self - } -} - -impl InteractiveElement for Chat { +impl InteractiveElement for ChatItem { fn interactivity(&mut self) -> &mut gpui::Interactivity { self.base.interactivity() } } -impl RenderOnce for Chat { +impl RenderOnce for ChatItem { fn render(self, cx: &mut WindowContext) -> impl IntoElement { - let ago = ago(self.item.last_seen.as_u64()); + let ago = ago(self.last_seen.as_u64()); + let fallback_name = show_npub(self.public_key, 16); let mut content = div() .font_medium() .text_color(cx.theme().sidebar_accent_foreground); - if let Some(metadata) = self.item.metadata.clone() { + if let Some(metadata) = self.metadata.clone() { content = content .flex() .items_center() @@ -108,16 +84,14 @@ impl RenderOnce for Chat { ) } else { this.flex_shrink_0() - .child(div().size_6().rounded_full().bg(cx.theme().muted)) + .child(img("brand/avatar.png").size_6().rounded_full()) } }) .map(|this| { if let Some(display_name) = metadata.display_name { this.child(display_name) - } else if let Ok(npub) = show_npub(self.item.public_key, 16) { - this.child(npub) } else { - this.child("Anon") + this.child(fallback_name) } }) } else { @@ -126,13 +100,12 @@ impl RenderOnce for Chat { .items_center() .gap_2() .child( - div() + img("brand/avatar.png") .flex_shrink_0() .size_6() - .rounded_full() - .bg(cx.theme().muted), + .rounded_full(), ) - .child("Anon") + .child(fallback_name) } self.base @@ -156,9 +129,95 @@ impl RenderOnce for Chat { ) .on_click(move |_, cx| { cx.dispatch_action(Box::new(AddPanel { - title: self.item.title.clone(), - receiver: self.item.public_key, + title: self.title.clone(), + receiver: self.public_key, })) }) } } + +pub struct Chat { + title: Option, + public_key: PublicKey, + metadata: Model>, + last_seen: Timestamp, +} + +impl Chat { + pub fn new(event: Event, cx: &mut ViewContext<'_, Self>) -> Self { + let public_key = event.pubkey; + let last_seen = event.created_at; + + let metadata = cx.new_model(|_| None); + let async_metadata = metadata.clone(); + + let mut async_cx = cx.to_async(); + + cx.foreground_executor() + .spawn(async move { + let client = get_client(); + let query = async_cx + .background_executor() + .spawn(async move { client.database().metadata(public_key).await }) + .await; + + if let Ok(metadata) = query { + _ = async_cx.update_model(&async_metadata, |a, b| { + *a = metadata; + b.notify(); + }); + }; + }) + .detach(); + + 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(); + } + } +} + +impl Render for Chat { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let metadata = self.metadata.read(cx).clone(); + + div().child(ChatItem::new( + self.public_key, + metadata, + self.last_seen, + self.title.clone(), + )) + } +} diff --git a/crates/app/src/views/dock/inbox/mod.rs b/crates/app/src/views/dock/inbox/mod.rs index 33f7818..dffefe1 100644 --- a/crates/app/src/views/dock/inbox/mod.rs +++ b/crates/app/src/views/dock/inbox/mod.rs @@ -1,102 +1,60 @@ -use chat::{Chat, ChatDelegate}; -use coop_ui::{theme::ActiveTheme, v_flex, StyledExt}; +use chat::Chat; +use coop_ui::{theme::ActiveTheme, v_flex, Collapsible, Icon, IconName, StyledExt}; use gpui::*; -use itertools::Itertools; -use nostr_sdk::prelude::*; -use std::{cmp::Reverse, time::Duration}; +use prelude::FluentBuilder; -use crate::{get_client, states::account::AccountState}; +use crate::states::chat::ChatRegistry; pub mod chat; pub struct Inbox { label: SharedString, - chats: Model>>, + chats: Model>>>, + is_collapsed: bool, } impl Inbox { pub fn new(cx: &mut ViewContext<'_, Self>) -> Self { let chats = cx.new_model(|_| None); - let async_chats = chats.clone(); - if let Some(public_key) = cx.global::().in_use { - let client = get_client(); - let filter = Filter::new() - .kind(Kind::PrivateDirectMessage) - .pubkey(public_key); - - let mut async_cx = cx.to_async(); - - cx.foreground_executor() - .spawn(async move { - let events = async_cx - .background_executor() - .spawn(async move { - if let Ok(events) = client.database().query(vec![filter]).await { - events - .into_iter() - .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() - } - }) - .await; - - // Get all public keys - let public_keys: Vec = - events.iter().map(|event| event.pubkey).collect(); - - // Calculate total public keys - let total = public_keys.len(); - - // Create subscription for metadata events - let filter = Filter::new() - .kind(Kind::Metadata) - .authors(public_keys) - .limit(total); - - let mut chats = Vec::new(); - let mut stream = async_cx - .background_executor() - .spawn(async move { - client - .stream_events(vec![filter], Some(Duration::from_secs(15))) - .await - .unwrap() - }) - .await; - - while let Some(event) = stream.next().await { - // TODO: generate some random name? - let title = if let Some(tag) = event.tags.find(TagKind::Title) { - tag.content().map(|s| s.to_string()) - } else { - None - }; - - let metadata = Metadata::from_json(event.content).ok(); - let chat = - ChatDelegate::new(title, event.pubkey, metadata, event.created_at); - - chats.push(chat); - } - - _ = async_cx.update_model(&async_chats, |a, b| { - *a = Some(chats); - b.notify(); - }); - }) - .detach(); - } + cx.observe_global::(|inbox, cx| { + inbox.add_chats(cx); + }) + .detach(); Self { - label: "Inbox".into(), chats, + label: "Inbox".into(), + is_collapsed: false, } } + + fn add_chats(&self, cx: &mut ViewContext) { + let events = cx.global::().get(cx); + + if let Some(events) = events { + let chats: Vec> = events + .into_iter() + .map(|event| cx.new_view(|cx| Chat::new(event, cx))) + .collect(); + + cx.update_model(&self.chats, |a, b| { + *a = Some(chats); + b.notify(); + }); + } + } +} + +impl Collapsible for Inbox { + fn is_collapsed(&self) -> bool { + self.is_collapsed + } + + fn collapsed(mut self, collapsed: bool) -> Self { + self.is_collapsed = collapsed; + self + } } impl Render for Inbox { @@ -104,25 +62,38 @@ impl Render for Inbox { let mut content = div(); if let Some(chats) = self.chats.read(cx).as_ref() { - content = content.children(chats.iter().map(move |item| Chat::new(item.clone()))) + content = content.children(chats.clone()) } v_flex() - .pt_3() + .gap_1() + .pt_2() .px_2() - .gap_2() .child( div() .id("inbox") .h_7() + .px_1() .flex() .items_center() - .gap_2() + .rounded_md() .text_xs() .font_semibold() .text_color(cx.theme().sidebar_foreground.opacity(0.7)) + .hover(|this| this.bg(cx.theme().sidebar_accent.opacity(0.7))) + .on_click(cx.listener(move |view, _event, cx| { + view.is_collapsed = !view.is_collapsed; + cx.notify(); + })) + .child( + Icon::new(IconName::ChevronDown) + .size_6() + .when(self.is_collapsed, |this| { + this.rotate(percentage(270. / 360.)) + }), + ) .child(self.label.clone()), ) - .child(content) + .when(!self.is_collapsed, |this| this.child(content)) } } diff --git a/crates/app/src/views/dock/left_dock.rs b/crates/app/src/views/dock/left_dock.rs index 13ea581..f722a6d 100644 --- a/crates/app/src/views/dock/left_dock.rs +++ b/crates/app/src/views/dock/left_dock.rs @@ -29,12 +29,12 @@ impl LeftDock { let inbox = cx.new_view(Inbox::new); Self { - inbox, name: "Left Dock".into(), closeable: true, zoomable: true, focus_handle: cx.focus_handle(), view_id: cx.view().entity_id(), + inbox, } } } diff --git a/crates/app/src/views/dock/mod.rs b/crates/app/src/views/dock/mod.rs index cdf8959..4ce8240 100644 --- a/crates/app/src/views/dock/mod.rs +++ b/crates/app/src/views/dock/mod.rs @@ -1,5 +1,4 @@ pub mod chat; +pub mod inbox; pub mod left_dock; pub mod welcome; - -pub mod inbox; diff --git a/crates/app/src/views/onboarding.rs b/crates/app/src/views/onboarding.rs index bd43333..643fc79 100644 --- a/crates/app/src/views/onboarding.rs +++ b/crates/app/src/views/onboarding.rs @@ -7,7 +7,7 @@ use gpui::*; use keyring::Entry; use nostr_sdk::prelude::*; -use crate::{constants::KEYRING_SERVICE, get_client, states::account::AccountState}; +use crate::{constants::KEYRING_SERVICE, get_client, states::account::AccountRegistry}; pub struct Onboarding { input: View, @@ -50,8 +50,8 @@ impl Onboarding { }); // Update view - cx.update_global(|state: &mut AccountState, cx| { - state.in_use = Some(public_key); + cx.update_global(|state: &mut AccountRegistry, cx| { + state.set_user(Some(public_key)); cx.notify(); }); diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index 59405f8..b76ae37 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -283,8 +283,8 @@ impl ThemeColor { table_head_foreground: hsl(240.0, 10., 3.9).opacity(0.7), table_hover: hsl(240.0, 4.8, 95.0), table_row_border: hsl(240.0, 7.7, 94.5), - title_bar: hsl(0.0, 0.0, 100.), - title_bar_border: hsl(240.0, 5.9, 90.0), + title_bar: hsl(0.0, 0.0, 98.0), + title_bar_border: hsl(220.0, 13.0, 91.0), sidebar: hsl(0.0, 0.0, 98.0), sidebar_accent: hsl(240.0, 4.8, 92.), sidebar_accent_foreground: hsl(240.0, 5.9, 10.0), @@ -358,7 +358,7 @@ impl ThemeColor { table_head_foreground: hsl(0., 0., 78.).opacity(0.7), table_hover: hsl(240.0, 3.7, 15.9).opacity(0.5), table_row_border: hsl(240.0, 3.7, 16.9).opacity(0.5), - title_bar: hsl(0., 0., 9.7), + title_bar: hsl(240.0, 0.0, 10.0), title_bar_border: hsl(240.0, 3.7, 15.9), sidebar: hsl(240.0, 0.0, 10.0), sidebar_accent: hsl(240.0, 3.7, 15.9),