diff --git a/crates/app/src/constants.rs b/crates/app/src/constants.rs index 1d1ca35..185b09f 100644 --- a/crates/app/src/constants.rs +++ b/crates/app/src/constants.rs @@ -3,5 +3,5 @@ pub const APP_NAME: &str = "Coop"; pub const FAKE_SIG: &str = "f9e79d141c004977192d05a86f81ec7c585179c371f7350a5412d33575a2a356433f58e405c2296ed273e2fe0aafa25b641e39cc4e1f3f261ebf55bce0cbac83"; 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 = 100; +pub const METADATA_DELAY: u64 = 200; pub const IMAGE_SERVICE: &str = "https://wsrv.nl"; diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index 3a786ea..27417e4 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -8,7 +8,7 @@ use gpui::{ VisualContext, WindowBounds, WindowDecorations, WindowKind, WindowOptions, }; use nostr_sdk::prelude::*; -use states::{account::AccountRegistry, chat::ChatRegistry}; +use states::{app::AppRegistry, chat::ChatRegistry}; use std::{ collections::HashSet, fs, @@ -38,6 +38,8 @@ static CLIENT: OnceLock = OnceLock::new(); pub enum Signal { /// Receive event Event(Event), + /// Receive metadata + Metadata(PublicKey), /// Receive EOSE Eose, } @@ -99,7 +101,6 @@ async fn main() { let all_messages = SubscriptionId::new(ALL_MESSAGES_SUB_ID); while let Ok(notification) = notifications.recv().await { - #[allow(clippy::collapsible_match)] if let RelayPoolNotification::Message { message, .. } = notification { if let RelayMessage::Event { event, @@ -143,6 +144,10 @@ async fn main() { } Err(e) => println!("Unwrap error: {}", e), } + } else if event.kind == Kind::Metadata { + if let Err(e) = signal_tx.send(Signal::Metadata(event.pubkey)).await { + println!("Send error: {}", e) + } } } else if let RelayMessage::EndOfStoredEvents(subscription_id) = message { if subscription_id == all_messages { @@ -195,8 +200,8 @@ async fn main() { .with_assets(Assets) .with_http_client(Arc::new(reqwest_client::ReqwestClient::new())) .run(move |cx| { - // Account state - AccountRegistry::set_global(cx); + // App state + AppRegistry::set_global(cx); // Chat state ChatRegistry::set_global(cx); @@ -216,20 +221,23 @@ async fn main() { let hex = String::from_utf8(secret).unwrap(); let keys = Keys::parse(&hex).unwrap(); - _ = client.set_signer(keys).await; + // Update signer + async_cx + .background_executor() + .spawn(async move { client.set_signer(keys).await }) + .detach(); // Update global state - _ = async_cx.update_global::(|state, _cx| { - state.set_user(Some(public_key)); - state.set_loading(); + _ = async_cx.update_global::(|state, cx| { + state.set_user(public_key, cx); }); } else { - _ = async_cx.update_global::(|state, _| { + _ = async_cx.update_global::(|state, _| { state.set_loading(); }); } } else { - _ = async_cx.update_global::(|state, _| { + _ = async_cx.update_global::(|state, _| { state.set_loading(); }); } @@ -258,6 +266,11 @@ async fn main() { state.init(cx); }); } + Signal::Metadata(public_key) => { + _ = async_cx.update_global::(|state, cx| { + state.set_refresh(public_key, cx); + }); + } Signal::Event(event) => { let metadata = async_cx .background_executor() diff --git a/crates/app/src/states/account.rs b/crates/app/src/states/account.rs deleted file mode 100644 index 8797408..0000000 --- a/crates/app/src/states/account.rs +++ /dev/null @@ -1,85 +0,0 @@ -use gpui::*; -use nostr_sdk::prelude::*; -use std::time::Duration; - -use crate::{ - constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID}, - get_client, -}; - -pub struct AccountRegistry { - public_key: Option, - pub(crate) is_loading: bool, -} - -impl Global for AccountRegistry {} - -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.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() - .exit_policy(ReqExitPolicy::WaitDurationAfterEOSE(Duration::from_secs(5))); - - // Create a filter for getting new message - let new_message = Filter::new() - .kind(Kind::GiftWrap) - .pubkey(public_key) - .limit(0); - - cx.background_executor() - .spawn(async move { - // Subscribe for all messages - if client - .subscribe_with_id(all_messages_sub_id, vec![all_messages], Some(opts)) - .await - .is_ok() - { - // Subscribe for new message - _ = client - .subscribe_with_id(new_message_sub_id, vec![new_message], None) - .await - } - }) - .detach(); - } - }) - .detach(); - } - - pub fn set_loading(&mut self) { - self.is_loading = false - } - - pub fn get(&self) -> Option { - self.public_key - } - - 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 { - public_key: None, - is_loading: true, - } - } -} diff --git a/crates/app/src/states/app.rs b/crates/app/src/states/app.rs new file mode 100644 index 0000000..ceb8540 --- /dev/null +++ b/crates/app/src/states/app.rs @@ -0,0 +1,111 @@ +use crate::{ + constants::{ALL_MESSAGES_SUB_ID, NEW_MESSAGE_SUB_ID}, + get_client, +}; +use gpui::*; +use nostr_sdk::prelude::*; +use std::time::Duration; + +pub struct AppRegistry { + user: Model>, + refreshs: Model>, + pub(crate) is_loading: bool, +} + +impl Global for AppRegistry {} + +impl AppRegistry { + pub fn set_global(cx: &mut AppContext) { + let refreshs = cx.new_model(|_| Vec::new()); + let user = cx.new_model(|_| None); + let async_user = user.clone(); + + cx.set_global(Self { + user, + refreshs, + is_loading: true, + }); + + cx.observe(&async_user, |model, cx| { + if let Some(public_key) = model.read(cx).clone().as_ref() { + let client = get_client(); + let public_key = *public_key; + + 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() + .exit_policy(ReqExitPolicy::WaitDurationAfterEOSE(Duration::from_secs(5))); + + // Create a filter for getting new message + let new_message = Filter::new() + .kind(Kind::GiftWrap) + .pubkey(public_key) + .limit(0); + + cx.background_executor() + .spawn(async move { + // Subscribe for all messages + _ = client + .subscribe_with_id(all_messages_sub_id, vec![all_messages], Some(opts)) + .await; + + // Subscribe for new message + _ = client + .subscribe_with_id(new_message_sub_id, vec![new_message], None) + .await; + + 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) + .limit(1); + + // Get contact list + _ = client.sync(subscription, &SyncOptions::default()).await; + }) + .detach(); + } + }) + .detach(); + } + + pub fn set_loading(&mut self) { + self.is_loading = false + } + + pub fn set_user(&mut self, public_key: PublicKey, cx: &mut AppContext) { + self.user.update(cx, |model, cx| { + *model = Some(public_key); + cx.notify(); + }); + + self.is_loading = false; + } + + pub fn set_refresh(&mut self, public_key: PublicKey, cx: &mut AppContext) { + self.refreshs.update(cx, |this, cx| { + this.push(public_key); + cx.notify(); + }) + } + + pub fn current_user(&self) -> WeakModel> { + self.user.downgrade() + } + + pub fn refreshs(&self) -> WeakModel> { + self.refreshs.downgrade() + } +} diff --git a/crates/app/src/states/chat.rs b/crates/app/src/states/chat.rs index ef69e67..af1dda1 100644 --- a/crates/app/src/states/chat.rs +++ b/crates/app/src/states/chat.rs @@ -97,11 +97,8 @@ impl ChatRegistry { } pub fn init(&mut self, cx: &mut AppContext) { - if self.is_initialized { - return; - } - let async_cx = cx.to_async(); + // Get all current room's ids let ids: Vec = self .rooms @@ -113,42 +110,43 @@ impl ChatRegistry { 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 + let query: anyhow::Result, anyhow::Error> = 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(|ev| { - let new_id = get_room_id(&ev.pubkey, &ev.tags); - // Get new events only - !ids.iter().any(|id| id == &new_id) - }) // Filter all messages from current user - .unique_by(|ev| ev.pubkey) - .sorted_by_key(|ev| Reverse(ev.created_at)) - .collect::>() - } else { - Vec::new() - } + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; + + let filter = Filter::new() + .kind(Kind::PrivateDirectMessage) + .pubkey(public_key); + + let events = client.database().query(vec![filter]).await?; + let result = events + .into_iter() + .filter(|ev| ev.pubkey != public_key) + .filter(|ev| { + let new_id = get_room_id(&ev.pubkey, &ev.tags); + // Get new events only + !ids.iter().any(|id| id == &new_id) + }) // Filter all messages from current user + .unique_by(|ev| ev.pubkey) + .sorted_by_key(|ev| Reverse(ev.created_at)) + .collect::>(); + + Ok(result) }) .await; - _ = async_cx.update_global::(|state, cx| { - state.rooms.update(cx, |model, cx| { - model.extend(events); - cx.notify(); - }); + if let Ok(events) = query { + _ = async_cx.update_global::(|state, cx| { + state.rooms.update(cx, |model, cx| { + model.extend(events); + cx.notify(); + }); - state.is_initialized = true; - }); + state.is_initialized = true; + }); + } }) .detach(); } diff --git a/crates/app/src/states/mod.rs b/crates/app/src/states/mod.rs index d0f897b..f8379b1 100644 --- a/crates/app/src/states/mod.rs +++ b/crates/app/src/states/mod.rs @@ -1,2 +1,2 @@ -pub mod account; +pub mod app; pub mod chat; diff --git a/crates/app/src/views/account.rs b/crates/app/src/views/account.rs index 8c968dc..1aead04 100644 --- a/crates/app/src/views/account.rs +++ b/crates/app/src/views/account.rs @@ -10,12 +10,12 @@ use ui::{ Icon, IconName, Sizable, }; +use crate::states::app::AppRegistry; use crate::{constants::IMAGE_SERVICE, get_client}; actions!(account, [ToDo]); pub struct Account { - #[allow(dead_code)] public_key: PublicKey, metadata: Model>, } @@ -23,32 +23,46 @@ pub struct Account { impl Account { pub fn new(public_key: PublicKey, cx: &mut ViewContext<'_, Self>) -> Self { let metadata = cx.new_model(|_| None); - let async_metadata = metadata.clone(); + let refreshs = cx.global_mut::().refreshs(); - 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(); - }); - }; + if let Some(refreshs) = refreshs.upgrade() { + cx.observe(&refreshs, |this, _, cx| { + this.load_metadata(cx); }) .detach(); + } Self { public_key, metadata, } } + + pub fn load_metadata(&self, cx: &mut ViewContext) { + let mut async_cx = cx.to_async(); + let async_metadata = self.metadata.clone(); + + cx.foreground_executor() + .spawn({ + let client = get_client(); + let public_key = self.public_key; + + async move { + let metadata = async_cx + .background_executor() + .spawn(async move { client.database().metadata(public_key).await }) + .await; + + if let Ok(metadata) = metadata { + _ = async_cx.update_model(&async_metadata, |model, cx| { + *model = metadata; + cx.notify(); + }); + } + } + }) + .detach(); + } } impl Render for Account { @@ -59,20 +73,25 @@ impl Render for Account { .reverse() .ghost() .icon(Icon::new(IconName::ChevronDownSmall)) - .when_some(self.metadata.read(cx).as_ref(), |this, metadata| { - this.map(|this| { - if let Some(picture) = metadata.picture.clone() { - this.flex_shrink_0().child( - img(format!("{}/?url={}&w=72&h=72&n=-1", IMAGE_SERVICE, picture)) - .size_5() - .rounded_full() - .object_fit(ObjectFit::Cover), - ) - } else { - this.flex_shrink_0() - .child(img("brand/avatar.png").size_6().rounded_full()) - } - }) + .map(|this| { + if let Some(metadata) = self.metadata.read(cx).as_ref() { + this.map(|this| { + if let Some(picture) = metadata.picture.clone() { + this.flex_shrink_0().child( + img(format!("{}/?url={}&w=72&h=72&n=-1", IMAGE_SERVICE, picture)) + .size_5() + .rounded_full() + .object_fit(ObjectFit::Cover), + ) + } else { + this.flex_shrink_0() + .child(img("brand/avatar.png").size_5().rounded_full()) + } + }) + } else { + this.flex_shrink_0() + .child(img("brand/avatar.png").size_5().rounded_full()) + } }) .popup_menu(move |this, _cx| { this.menu("Profile", Box::new(ToDo)) diff --git a/crates/app/src/views/app.rs b/crates/app/src/views/app.rs index 84aea2d..fe2d70e 100644 --- a/crates/app/src/views/app.rs +++ b/crates/app/src/views/app.rs @@ -2,8 +2,7 @@ use super::{ account::Account, chat::ChatPanel, onboarding::Onboarding, sidebar::Sidebar, welcome::WelcomePanel, }; -use crate::states::{account::AccountRegistry, chat::Room}; -use gpui::prelude::FluentBuilder; +use crate::states::{app::AppRegistry, chat::Room}; use gpui::{ div, impl_actions, px, Axis, Context, Edges, InteractiveElement, IntoElement, Model, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, WindowContext, @@ -64,20 +63,31 @@ impl AppView { // Dock let dock = cx.new_view(|cx| DockArea::new(DOCK_AREA.id, Some(DOCK_AREA.version), cx)); - cx.observe_global::(move |view, cx| { - if let Some(public_key) = cx.global::().get() { - Self::init_layout(view.dock.downgrade(), cx); - // TODO: save dock state and load previous state on startup + // Get current user from app state + let current_user = cx.global::().current_user(); - let view = cx.new_view(|cx| Account::new(public_key, cx)); + if let Some(current_user) = current_user.upgrade() { + cx.observe(¤t_user, move |view, model, cx| { + if let Some(public_key) = model.read(cx).clone().as_ref() { + Self::init_layout(view.dock.downgrade(), cx); + // TODO: save dock state and load previous state on startup - cx.update_model(&async_account, |model, cx| { - *model = Some(view); - cx.notify(); - }); - } - }) - .detach(); + let view = cx.new_view(|cx| { + let view = Account::new(*public_key, cx); + // Initial load metadata + view.load_metadata(cx); + + view + }); + + cx.update_model(&async_account, |model, cx| { + *model = Some(view); + cx.notify(); + }); + } + }) + .detach(); + } AppView { account, @@ -138,11 +148,10 @@ impl Render for AppView { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let modal_layer = Root::render_modal_layer(cx); let notification_layer = Root::render_notification_layer(cx); - let state = cx.global::(); let mut content = div().size_full().flex().flex_col(); - if state.is_loading { + if cx.global::().is_loading { content = content.child(div()).child( div() .flex_1() @@ -151,7 +160,7 @@ impl Render for AppView { .justify_center() .child(Indicator::new().small()), ) - } else if state.is_user_logged_in() { + } else if let Some(account) = self.account.read(cx).as_ref() { content = content .child( TitleBar::new() @@ -165,9 +174,7 @@ impl Render for AppView { .justify_end() .gap_1() .px_2() - .when_some(self.account.read(cx).as_ref(), |this, account| { - this.child(account.clone()) - }), + .child(account.clone()), ), ) .child(self.dock.clone()) diff --git a/crates/app/src/views/chat/room.rs b/crates/app/src/views/chat/room.rs index c7f32b4..351ba29 100644 --- a/crates/app/src/views/chat/room.rs +++ b/crates/app/src/views/chat/room.rs @@ -15,10 +15,7 @@ use ui::{ use super::message::RoomMessage; use crate::{ get_client, - states::{ - account::AccountRegistry, - chat::{ChatRegistry, Room}, - }, + states::chat::{ChatRegistry, Room}, }; #[derive(Clone)] @@ -202,7 +199,6 @@ impl RoomPanel { fn send_message(&mut self, cx: &mut ViewContext) { let owner = self.owner; - let current_user = cx.global::().get().unwrap(); let content = self.input.read(cx).text().to_string(); let content2 = content.clone(); let content3 = content2.clone(); @@ -216,6 +212,14 @@ impl RoomPanel { let client = get_client(); async move { + let current_user = async_cx + .background_executor() + .spawn(async move { + let signer = client.signer().await.unwrap(); + signer.get_public_key().await.unwrap() + }) + .await; + // Send message to all members async_cx .background_executor() diff --git a/crates/app/src/views/onboarding/mod.rs b/crates/app/src/views/onboarding/mod.rs index 7bbc2ff..4fc0320 100644 --- a/crates/app/src/views/onboarding/mod.rs +++ b/crates/app/src/views/onboarding/mod.rs @@ -1,15 +1,11 @@ -use gpui::{ - div, IntoElement, - ParentElement, Render, Styled, View, ViewContext, VisualContext, -}; +use crate::{constants::KEYRING_SERVICE, get_client, states::app::AppRegistry}; +use gpui::{div, IntoElement, ParentElement, Render, Styled, View, ViewContext, VisualContext}; use nostr_sdk::prelude::*; use ui::{ input::{InputEvent, TextInput}, label::Label, }; -use crate::{constants::KEYRING_SERVICE, get_client, states::account::AccountRegistry}; - pub struct Onboarding { input: View, } @@ -40,7 +36,6 @@ impl Onboarding { let secret = keys.secret_key().to_secret_hex(); let mut async_cx = cx.to_async(); - let view_id = cx.entity_id(); cx.foreground_executor() .spawn({ @@ -50,10 +45,8 @@ impl Onboarding { async move { if task.await.is_ok() { _ = client.set_signer(keys).await; - // Update global state - _ = async_cx.update_global::(|state, cx| { - state.set_user(Some(public_key)); - cx.notify(Some(view_id)); + _ = async_cx.update_global::(|state, cx| { + state.set_user(public_key, cx); }); } } diff --git a/crates/app/src/views/sidebar/contact_list.rs b/crates/app/src/views/sidebar/contact_list.rs index 12002e4..e2e76fb 100644 --- a/crates/app/src/views/sidebar/contact_list.rs +++ b/crates/app/src/views/sidebar/contact_list.rs @@ -1,6 +1,4 @@ -use crate::{ - constants::IMAGE_SERVICE, get_client, states::account::AccountRegistry, utils::show_npub, -}; +use crate::{constants::IMAGE_SERVICE, get_client, utils::show_npub}; use gpui::{ div, img, impl_actions, list, px, Context, ElementId, FocusHandle, InteractiveElement, IntoElement, ListAlignment, ListState, Model, ParentElement, Pixels, Render, RenderOnce, @@ -24,12 +22,12 @@ impl_actions!(contacts, [SelectContact]); struct ContactListItem { id: ElementId, public_key: PublicKey, - metadata: Option, + metadata: Metadata, selected: bool, } impl ContactListItem { - pub fn new(public_key: PublicKey, metadata: Option) -> Self { + pub fn new(public_key: PublicKey, metadata: Metadata) -> Self { let id = SharedString::from(public_key.to_hex()).into(); Self { @@ -55,36 +53,6 @@ impl Selectable for ContactListItem { impl RenderOnce for ContactListItem { fn render(self, cx: &mut WindowContext) -> impl IntoElement { let fallback = show_npub(self.public_key, 16); - let mut content = div().flex().items_center().gap_2().text_sm(); - - if let Some(metadata) = self.metadata { - content = content - .map(|this| { - if let Some(picture) = metadata.picture { - this.flex_shrink_0().child( - img(format!( - "{}/?url={}&w=72&h=72&fit=cover&mask=circle&n=-1", - IMAGE_SERVICE, picture - )) - .size_6(), - ) - } else { - this.flex_shrink_0() - .child(img("brand/avatar.png").size_6().rounded_full()) - } - }) - .map(|this| { - if let Some(display_name) = metadata.display_name { - this.flex_1().child(display_name) - } else { - this.flex_1().child(fallback) - } - }) - } else { - content = content - .child(img("brand/avatar.png").size_6().rounded_full()) - .child(fallback) - } div() .id(self.id) @@ -95,7 +63,34 @@ impl RenderOnce for ContactListItem { .flex() .items_center() .justify_between() - .child(content) + .child( + div() + .flex() + .items_center() + .gap_2() + .text_sm() + .map(|this| { + if let Some(picture) = self.metadata.picture { + this.flex_shrink_0().child( + img(format!( + "{}/?url={}&w=72&h=72&fit=cover&mask=circle&n=-1", + IMAGE_SERVICE, picture + )) + .size_6(), + ) + } else { + this.flex_shrink_0() + .child(img("brand/avatar.png").size_6().rounded_full()) + } + }) + .map(|this| { + if let Some(display_name) = self.metadata.display_name { + this.flex_1().child(display_name) + } else { + this.flex_1().child(fallback) + } + }), + ) .when(self.selected, |this| { this.child( Icon::new(IconName::CircleCheck) @@ -141,20 +136,24 @@ impl ContactList { cx.foreground_executor() .spawn({ let client = get_client(); - let current_user = cx.global::().get(); async move { - if let Some(public_key) = current_user { - if let Ok(profiles) = async_cx - .background_executor() - .spawn(async move { client.database().contacts(public_key).await }) - .await - { - _ = async_cx.update_model(&async_contacts, |model, cx| { - *model = profiles; - cx.notify(); - }); - } + 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?; + + Ok(profiles) + }) + .await; + + if let Ok(profiles) = query { + _ = async_cx.update_model(&async_contacts, |model, cx| { + *model = profiles; + cx.notify(); + }); } } }) @@ -166,9 +165,7 @@ impl ContactList { count: profiles.len(), items: profiles .into_iter() - .map(|contact| { - ContactListItem::new(contact.public_key(), Some(contact.metadata())) - }) + .map(|contact| ContactListItem::new(contact.public_key(), contact.metadata())) .collect(), }; @@ -194,7 +191,7 @@ impl ContactList { } } - pub fn selected(&self) -> Vec { + pub fn _selected(&self) -> Vec { self.selected.clone().into_iter().collect() } @@ -210,7 +207,7 @@ impl ContactList { let public_key = contact.public_key(); let metadata = contact.metadata(); - ContactListItem::new(public_key, Some(metadata)) + ContactListItem::new(contact.public_key(), metadata) .selected(self.selected.contains(&public_key)) }) .collect(), @@ -238,7 +235,7 @@ impl Render for ContactList { .flex() .flex_col() .gap_1() - .child(div().font_semibold().child("Contacts")) + .child(div().font_semibold().text_sm().child("Contacts")) .child( div() .p_1() diff --git a/crates/app/src/views/sidebar/inbox.rs b/crates/app/src/views/sidebar/inbox.rs index d6fc515..e71ed06 100644 --- a/crates/app/src/views/sidebar/inbox.rs +++ b/crates/app/src/views/sidebar/inbox.rs @@ -1,10 +1,11 @@ use crate::{ constants::IMAGE_SERVICE, get_client, - states::chat::ChatRegistry, - states::chat::Room, - utils::get_room_id, - utils::{ago, show_npub}, + states::{ + app::AppRegistry, + chat::{ChatRegistry, Room}, + }, + utils::{ago, get_room_id, show_npub}, views::app::{AddPanel, PanelKind}, }; use gpui::prelude::FluentBuilder; @@ -20,12 +21,21 @@ use ui::{skeleton::Skeleton, theme::ActiveTheme, v_flex, Collapsible, Icon, Icon struct InboxListItem { id: SharedString, event: Event, - metadata: Option, + metadata: Model>, } impl InboxListItem { - pub fn new(event: Event, metadata: Option, _cx: &mut ViewContext<'_, Self>) -> Self { + pub fn new(event: Event, metadata: Option, cx: &mut ViewContext<'_, Self>) -> Self { let id = SharedString::from(get_room_id(&event.pubkey, &event.tags)); + let metadata = cx.new_model(|_| metadata); + let refreshs = cx.global_mut::().refreshs(); + + if let Some(refreshs) = refreshs.upgrade() { + cx.observe(&refreshs, |this, _, cx| { + this.load_metadata(cx); + }) + .detach(); + } Self { id, @@ -34,8 +44,35 @@ impl InboxListItem { } } + pub fn load_metadata(&self, cx: &mut ViewContext) { + let mut async_cx = cx.to_async(); + let async_metadata = self.metadata.clone(); + + cx.foreground_executor() + .spawn({ + let client = get_client(); + let public_key = self.event.pubkey; + + async move { + let metadata = async_cx + .background_executor() + .spawn(async move { client.database().metadata(public_key).await }) + .await; + + if let Ok(metadata) = metadata { + _ = async_cx.update_model(&async_metadata, |model, cx| { + *model = metadata; + cx.notify(); + }); + } + } + }) + .detach(); + } + pub fn action(&self, cx: &mut WindowContext<'_>) { - let room = Arc::new(Room::parse(&self.event, self.metadata.clone())); + let metadata = self.metadata.read(cx).clone(); + let room = Arc::new(Room::parse(&self.event, metadata)); cx.dispatch_action(Box::new(AddPanel { panel: PanelKind::Room(room), @@ -53,7 +90,7 @@ impl Render for InboxListItem { .font_medium() .text_color(cx.theme().sidebar_accent_foreground); - if let Some(metadata) = self.metadata.clone() { + if let Some(metadata) = self.metadata.read(cx).as_ref() { content = content .flex() .items_center() @@ -145,9 +182,6 @@ impl Inbox { } pub fn load(&mut self, cx: &mut ViewContext) { - // Hide loading indicator - self.set_loading(cx); - // Get all room's events let events: Vec = cx.global::().rooms.read(cx).clone(); @@ -179,15 +213,13 @@ impl Inbox { *model = Some(views); cx.notify() }); + + this.is_loading = false; + cx.notify(); }); }) .detach(); } - - fn set_loading(&mut self, cx: &mut ViewContext) { - self.is_loading = false; - cx.notify(); - } } impl Collapsible for Inbox { diff --git a/crates/app/src/views/sidebar/mod.rs b/crates/app/src/views/sidebar/mod.rs index d68326c..25efcf4 100644 --- a/crates/app/src/views/sidebar/mod.rs +++ b/crates/app/src/views/sidebar/mod.rs @@ -57,8 +57,8 @@ impl Sidebar { .rounded(ButtonRounded::Large) .w_full() .on_click({ - let contact_list = contact_list.clone(); - move |_, cx| { + let _contact_list = contact_list.clone(); + move |_, _cx| { // TODO: open room } }),