feat: add image cache

This commit is contained in:
2025-04-23 08:16:26 +07:00
parent 86eca5803f
commit 73b2eac080
5 changed files with 243 additions and 118 deletions

View File

@@ -2,8 +2,9 @@ use account::Account;
use anyhow::Error;
use global::get_client;
use gpui::{
div, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis, Context, Entity,
InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription, Task, Window,
div, image_cache, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis,
Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription,
Task, Window,
};
use nostr_sdk::prelude::*;
use serde::Deserialize;
@@ -16,9 +17,14 @@ use ui::{
ContextModal, IconName, Root, Sizable, TitleBar,
};
use crate::views::{chat, compose, contacts, login, new_account, profile, relays, welcome};
use crate::views::{onboarding, sidebar};
use crate::{
lru_cache::cache_provider,
views::{
chat, compose, contacts, login, new_account, onboarding, profile, relays, sidebar, welcome,
},
};
const CACHE_SIZE: usize = 200;
const MODAL_WIDTH: f32 = 420.;
const SIDEBAR_WIDTH: f32 = 280.;
@@ -288,56 +294,62 @@ impl Render for ChatSpace {
.relative()
.size_full()
.child(
div()
.flex()
.flex_col()
image_cache(cache_provider("image-cache", CACHE_SIZE))
.size_full()
// Title Bar
.when(self.titlebar, |this| {
this.child(
TitleBar::new()
// Left side
.child(div())
// Right side
.child(
div()
.flex()
.items_center()
.justify_end()
.gap_2()
.px_2()
.child(
div()
.flex()
.flex_col()
.size_full()
// Title Bar
.when(self.titlebar, |this| {
this.child(
TitleBar::new()
// Left side
.child(div())
// Right side
.child(
Button::new("appearance")
.xsmall()
.ghost()
.map(|this| {
if cx.theme().appearance.is_dark() {
this.icon(IconName::Sun)
} else {
this.icon(IconName::Moon)
}
})
.on_click(cx.listener(|_, _, window, cx| {
if cx.theme().appearance.is_dark() {
Theme::change(
Appearance::Light,
Some(window),
cx,
);
} else {
Theme::change(
Appearance::Dark,
Some(window),
cx,
);
}
})),
div()
.flex()
.items_center()
.justify_end()
.gap_2()
.px_2()
.child(
Button::new("appearance")
.xsmall()
.ghost()
.map(|this| {
if cx.theme().appearance.is_dark() {
this.icon(IconName::Sun)
} else {
this.icon(IconName::Moon)
}
})
.on_click(cx.listener(
|_, _, window, cx| {
if cx.theme().appearance.is_dark() {
Theme::change(
Appearance::Light,
Some(window),
cx,
);
} else {
Theme::change(
Appearance::Dark,
Some(window),
cx,
);
}
},
)),
),
),
),
)
})
// Dock
.child(self.dock.clone()),
)
})
// Dock
.child(self.dock.clone()),
),
)
// Notifications
.child(div().absolute().top_8().children(notification_layer))

View File

@@ -0,0 +1,117 @@
use std::{collections::HashMap, sync::Arc};
use futures::FutureExt;
use gpui::{
hash, AnyImageCache, App, AppContext, Asset, AssetLogger, Context, ElementId, Entity,
ImageAssetLoader, ImageCache, ImageCacheProvider, Window,
};
pub fn cache_provider(id: impl Into<ElementId>, max_items: usize) -> LruCacheProvider {
LruCacheProvider {
id: id.into(),
max_items,
}
}
pub struct LruCacheProvider {
id: ElementId,
max_items: usize,
}
impl ImageCacheProvider for LruCacheProvider {
fn provide(&mut self, window: &mut Window, cx: &mut App) -> AnyImageCache {
window
.with_global_id(self.id.clone(), |global_id, window| {
window.with_element_state::<Entity<LruCache>, _>(global_id, |lru_cache, _window| {
let mut lru_cache =
lru_cache.unwrap_or_else(|| cx.new(|cx| LruCache::new(self.max_items, cx)));
if lru_cache.read(cx).max_items != self.max_items {
lru_cache = cx.new(|cx| LruCache::new(self.max_items, cx));
}
(lru_cache.clone(), lru_cache)
})
})
.into()
}
}
struct LruCache {
max_items: usize,
usages: Vec<u64>,
cache: HashMap<u64, gpui::ImageCacheItem>,
}
impl LruCache {
fn new(max_items: usize, cx: &mut Context<Self>) -> Self {
cx.on_release(|simple_cache, cx| {
for (_, mut item) in std::mem::take(&mut simple_cache.cache) {
if let Some(Ok(image)) = item.get() {
cx.drop_image(image, None);
}
}
})
.detach();
Self {
max_items,
usages: Vec::with_capacity(max_items),
cache: HashMap::with_capacity(max_items),
}
}
}
impl ImageCache for LruCache {
fn load(
&mut self,
resource: &gpui::Resource,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<gpui::RenderImage>, gpui::ImageCacheError>> {
assert_eq!(self.usages.len(), self.cache.len());
assert!(self.cache.len() <= self.max_items);
let hash = hash(resource);
if let Some(item) = self.cache.get_mut(&hash) {
let current_ix = self
.usages
.iter()
.position(|item| *item == hash)
.expect("cache and usages must stay in sync");
self.usages.remove(current_ix);
self.usages.insert(0, hash);
return item.get();
}
let fut = AssetLogger::<ImageAssetLoader>::load(resource.clone(), cx);
let task = cx.background_executor().spawn(fut).shared();
if self.usages.len() == self.max_items {
let oldest = self.usages.pop().unwrap();
let mut image = self
.cache
.remove(&oldest)
.expect("cache and usages must be in sync");
if let Some(Ok(image)) = image.get() {
cx.drop_image(image, Some(window));
}
}
self.cache
.insert(hash, gpui::ImageCacheItem::Loading(task.clone()));
self.usages.insert(0, hash);
let entity = window.current_view();
window
.spawn(cx, {
async move |cx| {
_ = task.await;
cx.on_next_frame(move |_, cx| {
cx.notify(entity);
});
}
})
.detach();
None
}
}

View File

@@ -28,6 +28,7 @@ use ui::{theme::Theme, Root};
pub(crate) mod asset;
pub(crate) mod chatspace;
pub(crate) mod lru_cache;
pub(crate) mod views;
actions!(coop, [Quit]);

View File

@@ -226,7 +226,6 @@ impl Chat {
return;
}
// temporarily disable message input
self.input.update(cx, |this, cx| {
this.set_loading(true, window, cx);
this.set_disabled(true, window, cx);
@@ -235,39 +234,35 @@ impl Chat {
let room = self.room.read(cx);
let task = room.send_message(content, cx);
cx.spawn_in(window, async move |this, cx| {
match task.await {
Ok(reports) => {
cx.update(|window, cx| {
this.update(cx, |this, cx| {
// Reset message input
this.input.update(cx, |this, cx| {
this.set_loading(false, window, cx);
this.set_disabled(false, window, cx);
this.set_text("", window, cx);
cx.notify();
});
})
.ok();
for item in reports.into_iter() {
window.push_notification(
Notification::error(item).title("Message Failed to Send"),
cx,
);
}
cx.spawn_in(window, async move |this, cx| match task.await {
Ok(reports) => {
cx.update(|window, cx| {
this.update(cx, |this, cx| {
this.input.update(cx, |this, cx| {
this.set_loading(false, window, cx);
this.set_disabled(false, window, cx);
this.set_text("", window, cx);
});
})
.ok();
}
Err(e) => {
cx.update(|window, cx| {
for item in reports.into_iter() {
window.push_notification(
Notification::error(e.to_string()).title("Message Failed to Send"),
Notification::error(item).title("Message Failed to Send"),
cx,
);
})
.ok();
}
}
})
.ok();
}
Err(e) => {
cx.update(|window, cx| {
window.push_notification(
Notification::error(e.to_string()).title("Message Failed to Send"),
cx,
);
})
.ok();
}
})
.detach();