feat: add empty and loading states for the inbox section

This commit is contained in:
2025-02-26 08:01:45 +07:00
parent 29ec6da872
commit 81664e3d4e
2 changed files with 115 additions and 64 deletions

View File

@@ -1,15 +1,16 @@
use chats::{registry::ChatRegistry, room::Room}; use chats::{registry::ChatRegistry, room::Room};
use compose::Compose; use compose::Compose;
use gpui::{ use gpui::{
div, img, percentage, prelude::FluentBuilder, px, uniform_list, AnyElement, App, AppContext, div, img, percentage, prelude::FluentBuilder, px, relative, uniform_list, AnyElement, App,
Context, Div, Empty, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, AppContext, Context, Div, Empty, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ParentElement, Render, SharedString, Stateful, StatefulInteractiveElement, Styled, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Stateful,
Window, StatefulInteractiveElement, Styled, Window,
}; };
use ui::{ use ui::{
button::{Button, ButtonRounded, ButtonVariants}, button::{Button, ButtonRounded, ButtonVariants},
dock_area::panel::{Panel, PanelEvent}, dock_area::panel::{Panel, PanelEvent},
popup_menu::PopupMenu, popup_menu::PopupMenu,
skeleton::Skeleton,
theme::{scale::ColorScaleStep, ActiveTheme}, theme::{scale::ColorScaleStep, ActiveTheme},
ContextModal, Disableable, Icon, IconName, Sizable, StyledExt, ContextModal, Disableable, Icon, IconName, Sizable, StyledExt,
}; };
@@ -136,6 +137,20 @@ impl Sidebar {
}) })
} }
fn render_skeleton(&self, total: i32) -> impl IntoIterator<Item = impl IntoElement> {
(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<Self>) { fn open(&self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
window.dispatch_action( window.dispatch_action(
Box::new(AddPanel::new( Box::new(AddPanel::new(
@@ -261,9 +276,44 @@ impl Render for Sidebar {
this.flex_1() this.flex_1()
.w_full() .w_full()
.when_some(ChatRegistry::global(cx), |this, state| { .when_some(ChatRegistry::global(cx), |this, state| {
let is_loading = state.read(cx).is_loading();
let rooms = state.read(cx).rooms(); let rooms = state.read(cx).rooms();
let len = rooms.len(); let len = rooms.len();
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."),
),
)
} else {
this.child( this.child(
uniform_list( uniform_list(
entity, entity,
@@ -283,6 +333,7 @@ impl Render for Sidebar {
) )
.size_full(), .size_full(),
) )
}
}) })
}), }),
) )

View File

@@ -1,7 +1,7 @@
use crate::room::{IncomingEvent, Room}; use crate::room::{IncomingEvent, Room};
use anyhow::anyhow; use anyhow::anyhow;
use common::{last_seen::LastSeen, utils::room_hash}; 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 itertools::Itertools;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use state::get_client; use state::get_client;
@@ -61,10 +61,8 @@ impl ChatRegistry {
pub fn load_chat_rooms(&mut self, cx: &mut Context<Self>) { pub fn load_chat_rooms(&mut self, cx: &mut Context<Self>) {
let client = get_client(); let client = get_client();
let (tx, rx) = oneshot::channel::<Option<Vec<Event>>>();
cx.background_spawn(async move { let task: Task<Result<Vec<Event>, Error>> = cx.background_spawn(async move {
let result = async {
let signer = client.signer().await?; let signer = client.signer().await?;
let public_key = signer.get_public_key().await?; let public_key = signer.get_public_key().await?;
@@ -78,12 +76,8 @@ impl ChatRegistry {
let send_events = client.database().query(send).await?; let send_events = client.database().query(send).await?;
let recv_events = client.database().query(recv).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;
if let Ok(events) = result {
let result: Vec<Event> = events let result: Vec<Event> = events
.into_iter() .into_iter()
.filter(|ev| ev.tags.public_keys().peekable().peek().is_some()) .filter(|ev| ev.tags.public_keys().peekable().peek().is_some())
@@ -91,25 +85,22 @@ impl ChatRegistry {
.sorted_by_key(|ev| Reverse(ev.created_at)) .sorted_by_key(|ev| Reverse(ev.created_at))
.collect(); .collect();
_ = tx.send(Some(result)); Ok(result)
} else { });
_ = tx.send(None);
}
})
.detach();
cx.spawn(|this, cx| async move { cx.spawn(|this, cx| async move {
if let Ok(Some(events)) = rx.await { if let Ok(events) = task.await {
cx.update(|cx| {
if !events.is_empty() { if !events.is_empty() {
_ = cx.update(|cx| { this.update(cx, |this, cx| {
_ = this.update(cx, |this, cx| { let mut rooms = this.rooms.write().unwrap();
let current_rooms = this.current_rooms_ids(cx); let current_ids = this.current_rooms_ids(cx);
let items: Vec<Entity<Room>> = events let items: Vec<Entity<Room>> = events
.into_iter() .into_iter()
.filter_map(|ev| { .filter_map(|ev| {
let new = room_hash(&ev); let new = room_hash(&ev);
// Filter all seen events // Filter all seen rooms
if !current_rooms.iter().any(|this| this == &new) { if !current_ids.iter().any(|this| this == &new) {
Some(Room::new(&ev, cx)) Some(Room::new(&ev, cx))
} else { } else {
None None
@@ -117,13 +108,22 @@ impl ChatRegistry {
}) })
.collect(); .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; this.is_loading = false;
cx.notify(); cx.notify();
}); })
}); .ok();
} else {
this.update(cx, |this, cx| {
this.is_loading = false;
cx.notify();
})
.ok();
} }
})
.ok();
} }
}) })
.detach(); .detach();