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 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<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>) {
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(),
)
}
})
}),
)

View File

@@ -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<Self>) {
let client = get_client();
let (tx, rx) = oneshot::channel::<Option<Vec<Event>>>();
cx.background_spawn(async move {
let result = async {
let signer = client.signer().await?;
let public_key = signer.get_public_key().await?;
let task: Task<Result<Vec<Event>, 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<Event> = 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<Event> = 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<Entity<Room>> = 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();