chore: improve performance (#42)

* use uniform list for rooms list

* move profile cache to outside gpui context

* update comment

* refactor

* refactor

* .

* .

* add avatar component

* .

* refactor

* .
This commit is contained in:
reya
2025-05-27 07:34:22 +07:00
committed by GitHub
parent 45564c7722
commit 0f884f8142
25 changed files with 1087 additions and 1373 deletions
+266 -311
View File
@@ -1,7 +1,4 @@
use std::{
collections::{BTreeSet, HashSet},
time::Duration,
};
use std::{collections::BTreeSet, ops::Range, time::Duration};
use account::Account;
use async_utility::task::spawn;
@@ -10,33 +7,30 @@ use chats::{
ChatRegistry,
};
use common::{debounced_delay::DebouncedDelay, profile::SharedProfile};
use folder::{Folder, FolderItem, Parent};
use common::{debounced_delay::DebouncedDelay, profile::RenderProfile};
use element::DisplayRoom;
use global::{constants::SEARCH_RELAYS, get_client};
use gpui::{
div, img, prelude::FluentBuilder, AnyElement, App, AppContext, Context, Entity, EventEmitter,
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, ScrollHandle,
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window,
div, prelude::FluentBuilder, rems, uniform_list, AnyElement, App, AppContext, Context, Entity,
EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, RetainAllImageCache,
SharedString, Styled, Subscription, Task, Window,
};
use itertools::Itertools;
use nostr_sdk::prelude::*;
use smallvec::{smallvec, SmallVec};
use theme::ActiveTheme;
use ui::{
button::{Button, ButtonCustomVariant, ButtonRounded, ButtonVariants},
dock_area::{
dock::DockPlacement,
panel::{Panel, PanelEvent},
},
avatar::Avatar,
button::{Button, ButtonRounded, ButtonVariants},
dock_area::panel::{Panel, PanelEvent},
input::{InputEvent, InputState, TextInput},
popup_menu::{PopupMenu, PopupMenuExt},
skeleton::Skeleton,
IconName, Sizable, StyledExt,
ContextModal, IconName, Selectable, Sizable, StyledExt,
};
use crate::chatspace::{AddPanel, ModalKind, PanelKind, ToggleModal};
use crate::chatspace::{ModalKind, ToggleModal};
mod folder;
mod element;
const FIND_DELAY: u64 = 600;
const FIND_LIMIT: usize = 10;
@@ -45,18 +39,6 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
Sidebar::new(window, cx)
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub enum Item {
Ongoing,
Incoming,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub enum SubItem {
Trusted,
Unknown,
}
pub struct Sidebar {
name: SharedString,
// Search
@@ -65,13 +47,12 @@ pub struct Sidebar {
finding: bool,
local_result: Entity<Option<Vec<Entity<Room>>>>,
global_result: Entity<Option<Vec<Entity<Room>>>>,
// Layout
folders: bool,
active_items: HashSet<Item>,
active_subitems: HashSet<SubItem>,
// Rooms
active_filter: Entity<RoomKind>,
trusted_only: bool,
// GPUI
focus_handle: FocusHandle,
scroll_handle: ScrollHandle,
image_cache: Entity<RetainAllImageCache>,
#[allow(dead_code)]
subscriptions: SmallVec<[Subscription; 1]>,
}
@@ -82,16 +63,7 @@ impl Sidebar {
}
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();
let scroll_handle = ScrollHandle::default();
let mut active_items = HashSet::with_capacity(2);
active_items.insert(Item::Ongoing);
let mut active_subitems = HashSet::with_capacity(2);
active_subitems.insert(SubItem::Trusted);
active_subitems.insert(SubItem::Unknown);
let active_filter = cx.new(|_| RoomKind::Ongoing);
let local_result = cx.new(|_| None);
let global_result = cx.new(|_| None);
@@ -100,8 +72,10 @@ impl Sidebar {
let mut subscriptions = smallvec![];
subscriptions.push(
cx.subscribe_in(&find_input, window, |this, _, event, _, cx| {
subscriptions.push(cx.subscribe_in(
&find_input,
window,
|this, _state, event, _window, cx| {
match event {
InputEvent::PressEnter { .. } => this.search(cx),
InputEvent::Change(text) => {
@@ -119,44 +93,24 @@ impl Sidebar {
}
_ => {}
}
}),
);
},
));
Self {
name: "Chat Sidebar".into(),
folders: false,
focus_handle: cx.focus_handle(),
image_cache: RetainAllImageCache::new(cx),
find_debouncer: DebouncedDelay::new(),
finding: false,
trusted_only: false,
active_filter,
find_input,
local_result,
global_result,
active_items,
active_subitems,
focus_handle,
scroll_handle,
subscriptions,
}
}
fn toggle_item(&mut self, item: Item, cx: &mut Context<Self>) {
if !self.active_items.remove(&item) {
self.active_items.insert(item);
}
cx.notify();
}
fn toggle_subitem(&mut self, subitem: SubItem, cx: &mut Context<Self>) {
if !self.active_subitems.remove(&subitem) {
self.active_subitems.insert(subitem);
}
cx.notify();
}
fn toggle_folder(&mut self, cx: &mut Context<Self>) {
self.folders = !self.folders;
cx.notify();
}
fn debounced_search(&self, cx: &mut Context<Self>) -> Task<()> {
cx.spawn(async move |this, cx| {
this.update(cx, |this, cx| {
@@ -189,7 +143,7 @@ impl Sidebar {
spawn(async move {
let client = get_client();
let signer = client.signer().await.unwrap();
let signer = client.signer().await.expect("signer is required");
for event in events.into_iter() {
let metadata = Metadata::from_json(event.content).unwrap_or_default();
@@ -227,6 +181,14 @@ impl Sidebar {
return;
}
if query.starts_with("nevent1")
|| query.starts_with("naddr")
|| query.starts_with("nsec1")
|| query.starts_with("note1")
{
return;
}
// Return if search is in progress
if self.finding {
return;
@@ -299,83 +261,151 @@ impl Sidebar {
});
}
fn push_room(&mut self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
if let Some(result) = self.global_result.read(cx).as_ref() {
if let Some(room) = result.iter().find(|this| this.read(cx).id == id).cloned() {
ChatRegistry::global(cx).update(cx, |this, cx| {
this.push_room(room, cx);
});
window.dispatch_action(
Box::new(AddPanel::new(PanelKind::Room(id), DockPlacement::Center)),
cx,
);
self.clear_search_results(cx);
}
}
fn filter(&self, kind: &RoomKind, cx: &Context<Self>) -> bool {
self.active_filter.read(cx) == kind
}
fn set_filter(&mut self, kind: RoomKind, cx: &mut Context<Self>) {
self.active_filter.update(cx, |this, cx| {
*this = kind;
cx.notify();
})
}
fn set_trusted_only(&mut self, cx: &mut Context<Self>) {
self.trusted_only = !self.trusted_only;
cx.notify();
}
fn open_room(&mut self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
let room = if let Some(room) = ChatRegistry::get_global(cx).room(&id, cx) {
room
} else {
self.clear_search_results(cx);
let Some(result) = self.global_result.read(cx).as_ref() else {
window.push_notification("Failed to open room. Please try again later.", cx);
return;
};
let Some(room) = result.iter().find(|this| this.read(cx).id == id).cloned() else {
window.push_notification("Failed to open room. Please try again later.", cx);
return;
};
room
};
ChatRegistry::global(cx).update(cx, |this, cx| {
this.push_room(room, cx);
});
}
fn render_account(&self, profile: &Profile, cx: &Context<Self>) -> impl IntoElement {
div()
.px_3()
.h_8()
.flex_none()
.flex()
.justify_between()
.items_center()
.child(
div()
.flex()
.items_center()
.gap_2()
.text_sm()
.font_semibold()
.child(Avatar::new(profile.render_avatar()).size(rems(1.75)))
.child(profile.render_name()),
)
.child(
div()
.flex()
.items_center()
.gap_2()
.child(
Button::new("user")
.icon(IconName::Ellipsis)
.small()
.ghost()
.rounded(ButtonRounded::Full)
.popup_menu(|this, _window, _cx| {
this.menu(
"Profile",
Box::new(ToggleModal {
modal: ModalKind::Profile,
}),
)
.menu(
"Relays",
Box::new(ToggleModal {
modal: ModalKind::Relay,
}),
)
}),
)
.child(
Button::new("compose")
.icon(IconName::PlusFill)
.tooltip("Create DM or Group DM")
.small()
.primary()
.rounded(ButtonRounded::Full)
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(
Box::new(ToggleModal {
modal: ModalKind::Compose,
}),
cx,
);
})),
),
)
}
fn render_skeleton(&self, total: i32) -> impl IntoIterator<Item = impl IntoElement> {
(0..total).map(|_| {
div()
.h_8()
.h_9()
.w_full()
.px_2()
.px_1p5()
.flex()
.items_center()
.gap_2()
.child(Skeleton::new().flex_shrink_0().size_6().rounded_full())
.child(Skeleton::new().w_20().h_3().rounded_sm())
.child(Skeleton::new().w_40().h_4().rounded_sm())
})
}
fn render_global_items(rooms: &[Entity<Room>], cx: &Context<Self>) -> Vec<FolderItem> {
let mut items = Vec::with_capacity(rooms.len());
fn render_uniform_item(
&self,
rooms: &[Entity<Room>],
range: Range<usize>,
cx: &Context<Self>,
) -> Vec<impl IntoElement> {
let mut items = Vec::with_capacity(range.end - range.start);
for room in rooms.iter() {
let this = room.read(cx);
let id = this.id;
let label = this.display_name(cx);
let img = this.display_image(cx).map(img);
for ix in range {
if let Some(room) = rooms.get(ix) {
let this = room.read(cx);
let id = this.id;
let ago = this.ago();
let label = this.display_name(cx);
let img = this.display_image(cx);
let item = FolderItem::new(id as usize)
.label(label)
.img(img)
.on_click({
cx.listener(move |this, _, window, cx| {
this.push_room(id, window, cx);
})
let handler = cx.listener(move |this, _, window, cx| {
this.open_room(id, window, cx);
});
items.push(item);
}
items
}
fn render_items(rooms: &[Entity<Room>], cx: &Context<Self>) -> Vec<FolderItem> {
let mut items = Vec::with_capacity(rooms.len());
for room in rooms.iter() {
let room = room.read(cx);
let id = room.id;
let ago = room.ago();
let label = room.display_name(cx);
let img = room.display_image(cx).map(img);
let item = FolderItem::new(id as usize)
.label(label)
.description(ago)
.img(img)
.on_click({
cx.listener(move |_, _, window, cx| {
window.dispatch_action(
Box::new(AddPanel::new(PanelKind::Room(id), DockPlacement::Center)),
cx,
);
})
});
items.push(item);
items.push(
DisplayRoom::new(ix)
.img(img)
.label(label)
.description(ago)
.on_click(handler),
)
}
}
items
@@ -409,220 +439,145 @@ impl Focusable for Sidebar {
}
impl Render for Sidebar {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let account = Account::get_global(cx).profile_ref();
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let chats = ChatRegistry::get_global(cx);
// Get search result
let local_result = self.local_result.read(cx);
let global_result = self.global_result.read(cx);
let rooms = if let Some(results) = self.local_result.read(cx) {
results.to_owned()
} else {
#[allow(clippy::collapsible_else_if)]
if self.active_filter.read(cx) == &RoomKind::Ongoing {
chats.ongoing_rooms(cx)
} else {
chats.request_rooms(self.trusted_only, cx)
}
};
div()
.id("sidebar")
.track_focus(&self.focus_handle)
.track_scroll(&self.scroll_handle)
.overflow_y_scroll()
.image_cache(self.image_cache.clone())
.size_full()
.flex()
.flex_col()
.gap_3()
.py_1()
.when_some(account, |this, profile| {
this.child(
div()
.px_3()
.h_7()
.flex_none()
.flex()
.justify_between()
.items_center()
.child(
div()
.flex()
.items_center()
.gap_2()
.text_sm()
.font_semibold()
.child(img(profile.shared_avatar()).size_7())
.child(profile.shared_name()),
)
.child(
div()
.flex()
.items_center()
.gap_2()
.child(
Button::new("user")
.icon(IconName::Ellipsis)
.small()
.ghost()
.rounded(ButtonRounded::Full)
.popup_menu(|this, _window, _cx| {
this.menu(
"Profile",
Box::new(ToggleModal {
modal: ModalKind::Profile,
}),
)
.menu(
"Relays",
Box::new(ToggleModal {
modal: ModalKind::Relay,
}),
)
}),
)
.child(
Button::new("compose")
.icon(IconName::PlusFill)
.tooltip("Create DM or Group DM")
.small()
.primary()
.rounded(ButtonRounded::Full)
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(
Box::new(ToggleModal {
modal: ModalKind::Compose,
}),
cx,
);
})),
),
),
)
// Account
.when_some(Account::get_global(cx).profile_ref(), |this, profile| {
this.child(self.render_account(profile, cx))
})
// Search Input
.child(
div().px_3().h_7().flex_none().child(
div().px_3().w_full().h_7().flex_none().child(
TextInput::new(&self.find_input).small().suffix(
Button::new("find")
.icon(IconName::Search)
.tooltip("Press Enter to search")
.small()
.custom(
ButtonCustomVariant::new(window, cx)
.active(gpui::transparent_black())
.color(gpui::transparent_black())
.hover(gpui::transparent_black())
.foreground(cx.theme().text_placeholder),
),
.transparent()
.small(),
),
),
)
.when_some(global_result.as_ref(), |this, rooms| {
// Global Search Results
.when_some(self.global_result.read(cx).clone(), |this, rooms| {
this.child(
div()
.px_1()
.flex()
.flex_col()
.gap_1()
.children(Self::render_global_items(rooms, cx)),
div().px_1().w_full().flex_1().overflow_y_hidden().child(
uniform_list(
cx.entity(),
"results",
rooms.len(),
move |this, range, _window, cx| {
this.render_uniform_item(&rooms, range, cx)
},
)
.h_full(),
),
)
})
.child(
div()
.px_1()
.px_2()
.w_full()
.flex_1()
.overflow_y_hidden()
.flex()
.flex_col()
.gap_1()
.child(
div()
.mb_1()
.px_2()
.flex_none()
.px_1()
.w_full()
.h_9()
.flex()
.justify_between()
.items_center()
.text_sm()
.font_semibold()
.text_color(cx.theme().text_placeholder)
.child("Messages")
.justify_between()
.child(
Button::new("menu")
.tooltip("Toggle chat folders")
.map(|this| {
if self.folders {
this.icon(IconName::FilterFill)
} else {
this.icon(IconName::Filter)
}
})
.small()
.custom(
ButtonCustomVariant::new(window, cx)
.foreground(cx.theme().text_placeholder)
.color(cx.theme().ghost_element_background)
.hover(cx.theme().ghost_element_background)
.active(cx.theme().ghost_element_background),
)
.on_click(cx.listener(move |this, _, _, cx| {
this.toggle_folder(cx);
})),
),
)
.when(chats.wait_for_eose, |this| {
this.children(self.render_skeleton(6))
})
.map(|this| {
if let Some(rooms) = local_result {
this.children(Self::render_items(rooms, cx))
} else if !self.folders {
this.children(Self::render_items(&chats.rooms, cx))
} else {
this.child(
Folder::new("Ongoing")
.icon(IconName::Folder)
.tooltip("All ongoing conversations")
.collapsed(!self.active_items.contains(&Item::Ongoing))
.on_click(cx.listener(move |this, _, _, cx| {
this.toggle_item(Item::Ongoing, cx);
}))
.children(Self::render_items(
&chats.rooms_by_kind(RoomKind::Ongoing, cx),
cx,
)),
)
.child(
Parent::new("Incoming")
.icon(IconName::Folder)
.tooltip("Incoming messages")
.collapsed(!self.active_items.contains(&Item::Incoming))
.on_click(cx.listener(move |this, _, _, cx| {
this.toggle_item(Item::Incoming, cx);
}))
div()
.flex()
.items_center()
.gap_2()
.child(
Folder::new("Trusted")
.icon(IconName::Folder)
.tooltip("Incoming messages from trusted contacts")
.collapsed(
!self.active_subitems.contains(&SubItem::Trusted),
)
.on_click(cx.listener(move |this, _, _, cx| {
this.toggle_subitem(SubItem::Trusted, cx);
}))
.children(Self::render_items(
&chats.rooms_by_kind(RoomKind::Trusted, cx),
cx,
)),
Button::new("all")
.label("All")
.small()
.bold()
.secondary()
.rounded(ButtonRounded::Full)
.selected(self.filter(&RoomKind::Ongoing, cx))
.on_click(cx.listener(|this, _, _, cx| {
this.set_filter(RoomKind::Ongoing, cx);
})),
)
.child(
Folder::new("Unknown")
.icon(IconName::Folder)
.tooltip("Incoming messages from unknowns")
.collapsed(
!self.active_subitems.contains(&SubItem::Unknown),
)
.on_click(cx.listener(move |this, _, _, cx| {
this.toggle_subitem(SubItem::Unknown, cx);
}))
.children(Self::render_items(
&chats.rooms_by_kind(RoomKind::Unknown, cx),
cx,
)),
Button::new("requests")
.label("Requests")
.small()
.bold()
.secondary()
.rounded(ButtonRounded::Full)
.selected(!self.filter(&RoomKind::Ongoing, cx))
.on_click(cx.listener(|this, _, _, cx| {
this.set_filter(RoomKind::Unknown, cx);
})),
),
)
}
}),
.when(!self.filter(&RoomKind::Ongoing, cx), |this| {
this.child(
Button::new("trusted")
.tooltip("Only show rooms from trusted contacts")
.map(|this| {
if self.trusted_only {
this.icon(IconName::FilterFill)
} else {
this.icon(IconName::Filter)
}
})
.small()
.transparent()
.on_click(cx.listener(|this, _, _, cx| {
this.set_trusted_only(cx);
})),
)
}),
)
.when(chats.wait_for_eose, |this| {
this.child(
div()
.flex()
.flex_col()
.gap_1()
.children(self.render_skeleton(10)),
)
})
.child(
uniform_list(
cx.entity(),
"rooms",
rooms.len(),
move |this, range, _window, cx| {
this.render_uniform_item(&rooms, range, cx)
},
)
.h_full(),
),
)
}
}