From 81664e3d4ea36d1f7461e286f814c801f57cecc1 Mon Sep 17 00:00:00 2001 From: reya Date: Wed, 26 Feb 2025 08:01:45 +0700 Subject: [PATCH] feat: add empty and loading states for the inbox section --- crates/app/src/views/sidebar/mod.rs | 95 ++++++++++++++++++++++------- crates/chats/src/registry.rs | 84 ++++++++++++------------- 2 files changed, 115 insertions(+), 64 deletions(-) diff --git a/crates/app/src/views/sidebar/mod.rs b/crates/app/src/views/sidebar/mod.rs index 78d753b..52e0e02 100644 --- a/crates/app/src/views/sidebar/mod.rs +++ b/crates/app/src/views/sidebar/mod.rs @@ -1,15 +1,16 @@ use chats::{registry::ChatRegistry, room::Room}; use compose::Compose; use gpui::{ - div, img, percentage, prelude::FluentBuilder, px, uniform_list, AnyElement, App, AppContext, - Context, Div, Empty, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, - IntoElement, ParentElement, Render, SharedString, Stateful, StatefulInteractiveElement, Styled, - Window, + div, img, percentage, prelude::FluentBuilder, px, relative, uniform_list, AnyElement, App, + AppContext, Context, Div, Empty, Entity, EventEmitter, FocusHandle, Focusable, + InteractiveElement, IntoElement, ParentElement, Render, SharedString, Stateful, + StatefulInteractiveElement, Styled, Window, }; use ui::{ button::{Button, ButtonRounded, ButtonVariants}, dock_area::panel::{Panel, PanelEvent}, popup_menu::PopupMenu, + skeleton::Skeleton, theme::{scale::ColorScaleStep, ActiveTheme}, ContextModal, Disableable, Icon, IconName, Sizable, StyledExt, }; @@ -136,6 +137,20 @@ impl Sidebar { }) } + fn render_skeleton(&self, total: i32) -> impl IntoIterator { + (0..total).map(|_| { + div() + .h_8() + .w_full() + .px_1() + .flex() + .items_center() + .gap_2() + .child(Skeleton::new().flex_shrink_0().size_6().rounded_full()) + .child(Skeleton::new().w_20().h_3().rounded_sm()) + }) + } + fn open(&self, id: u64, window: &mut Window, cx: &mut Context) { window.dispatch_action( Box::new(AddPanel::new( @@ -261,28 +276,64 @@ impl Render for Sidebar { this.flex_1() .w_full() .when_some(ChatRegistry::global(cx), |this, state| { + let is_loading = state.read(cx).is_loading(); let rooms = state.read(cx).rooms(); let len = rooms.len(); - this.child( - uniform_list( - entity, - "rooms", - len, - move |this, range, _, cx| { - let mut items = vec![]; - - for ix in range { - if let Some(room) = rooms.get(ix) { - items.push(this.render_room(ix, room, cx)); - } - } - - items - }, + if is_loading { + this.children(self.render_skeleton(5)) + } else if rooms.is_empty() { + this.child( + div() + .px_1() + .w_full() + .h_20() + .flex() + .flex_col() + .items_center() + .justify_center() + .text_center() + .rounded(px(cx.theme().radius)) + .bg(cx.theme().base.step(cx, ColorScaleStep::THREE)) + .child( + div() + .text_xs() + .font_semibold() + .line_height(relative(1.2)) + .child("No chats"), + ) + .child( + div() + .text_xs() + .text_color( + cx.theme() + .base + .step(cx, ColorScaleStep::ELEVEN), + ) + .child("Recent chats will appear here."), + ), ) - .size_full(), - ) + } else { + this.child( + uniform_list( + entity, + "rooms", + len, + move |this, range, _, cx| { + let mut items = vec![]; + + for ix in range { + if let Some(room) = rooms.get(ix) { + items.push(this.render_room(ix, room, cx)); + } + } + + items + }, + ) + .size_full(), + ) + } }) }), ) diff --git a/crates/chats/src/registry.rs b/crates/chats/src/registry.rs index da70f5d..081b7b7 100644 --- a/crates/chats/src/registry.rs +++ b/crates/chats/src/registry.rs @@ -1,7 +1,7 @@ use crate::room::{IncomingEvent, Room}; use anyhow::anyhow; use common::{last_seen::LastSeen, utils::room_hash}; -use gpui::{App, AppContext, Context, Entity, Global, WeakEntity}; +use gpui::{App, AppContext, Context, Entity, Global, Task, WeakEntity}; use itertools::Itertools; use nostr_sdk::prelude::*; use state::get_client; @@ -61,55 +61,46 @@ impl ChatRegistry { pub fn load_chat_rooms(&mut self, cx: &mut Context) { let client = get_client(); - let (tx, rx) = oneshot::channel::>>(); - cx.background_spawn(async move { - let result = async { - let signer = client.signer().await?; - let public_key = signer.get_public_key().await?; + let task: Task, Error>> = cx.background_spawn(async move { + let signer = client.signer().await?; + let public_key = signer.get_public_key().await?; - let send = Filter::new() - .kind(Kind::PrivateDirectMessage) - .author(public_key); + let send = Filter::new() + .kind(Kind::PrivateDirectMessage) + .author(public_key); - let recv = Filter::new() - .kind(Kind::PrivateDirectMessage) - .pubkey(public_key); + let recv = Filter::new() + .kind(Kind::PrivateDirectMessage) + .pubkey(public_key); - let send_events = client.database().query(send).await?; - let recv_events = client.database().query(recv).await?; + let send_events = client.database().query(send).await?; + let recv_events = client.database().query(recv).await?; + let events = send_events.merge(recv_events); - Ok::<_, anyhow::Error>(send_events.merge(recv_events)) - } - .await; + let result: Vec = events + .into_iter() + .filter(|ev| ev.tags.public_keys().peekable().peek().is_some()) + .unique_by(room_hash) + .sorted_by_key(|ev| Reverse(ev.created_at)) + .collect(); - if let Ok(events) = result { - let result: Vec = events - .into_iter() - .filter(|ev| ev.tags.public_keys().peekable().peek().is_some()) - .unique_by(room_hash) - .sorted_by_key(|ev| Reverse(ev.created_at)) - .collect(); - - _ = tx.send(Some(result)); - } else { - _ = tx.send(None); - } - }) - .detach(); + Ok(result) + }); cx.spawn(|this, cx| async move { - if let Ok(Some(events)) = rx.await { - if !events.is_empty() { - _ = cx.update(|cx| { - _ = this.update(cx, |this, cx| { - let current_rooms = this.current_rooms_ids(cx); + if let Ok(events) = task.await { + cx.update(|cx| { + if !events.is_empty() { + this.update(cx, |this, cx| { + let mut rooms = this.rooms.write().unwrap(); + let current_ids = this.current_rooms_ids(cx); let items: Vec> = events .into_iter() .filter_map(|ev| { let new = room_hash(&ev); - // Filter all seen events - if !current_rooms.iter().any(|this| this == &new) { + // Filter all seen rooms + if !current_ids.iter().any(|this| this == &new) { Some(Room::new(&ev, cx)) } else { None @@ -117,13 +108,22 @@ impl ChatRegistry { }) .collect(); - this.rooms.write().unwrap().extend(items); + rooms.extend(items); + rooms.sort_by_key(|room| Reverse(room.read(cx).last_seen())); this.is_loading = false; cx.notify(); - }); - }); - } + }) + .ok(); + } else { + this.update(cx, |this, cx| { + this.is_loading = false; + cx.notify(); + }) + .ok(); + } + }) + .ok(); } }) .detach();