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

76
Cargo.lock generated
View File

@@ -485,9 +485,9 @@ dependencies = [
[[package]] [[package]]
name = "aws-lc-sys" name = "aws-lc-sys"
version = "0.28.1" version = "0.28.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ddeb19ee86cb16ecfc871e5b0660aff6285760957aaedda6284cf0e790d3769" checksum = "bfa9b6986f250236c27e5a204062434a773a13243d2ffc2955f37bdba4c5c6a1"
dependencies = [ dependencies = [
"bindgen 0.69.5", "bindgen 0.69.5",
"cc", "cc",
@@ -1135,7 +1135,7 @@ dependencies = [
[[package]] [[package]]
name = "collections" name = "collections"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#107d8ca483276263362c23482612d1cadea81c71" source = "git+https://github.com/zed-industries/zed#6a009b447af415f80cdb55d8c6064e984b899fd5"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
@@ -1203,7 +1203,7 @@ version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [ dependencies = [
"getrandom 0.2.15", "getrandom 0.2.16",
"once_cell", "once_cell",
"tiny-keccak", "tiny-keccak",
] ]
@@ -1472,9 +1472,9 @@ dependencies = [
[[package]] [[package]]
name = "ctor" name = "ctor"
version = "0.4.1" version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e9666f4a9a948d4f1dff0c08a4512b0f7c86414b23960104c243c10d79f4c3" checksum = "a4735f265ba6a1188052ca32d461028a7d1125868be18e287e756019da7607b5"
dependencies = [ dependencies = [
"ctor-proc-macro", "ctor-proc-macro",
"dtor", "dtor",
@@ -1524,7 +1524,7 @@ dependencies = [
[[package]] [[package]]
name = "derive_refineable" name = "derive_refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#107d8ca483276263362c23482612d1cadea81c71" source = "git+https://github.com/zed-industries/zed#6a009b447af415f80cdb55d8c6064e984b899fd5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1627,9 +1627,9 @@ dependencies = [
[[package]] [[package]]
name = "dtor" name = "dtor"
version = "0.0.5" version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "222ef136a1c687d4aa0395c175f2c4586e379924c352fd02f7870cf7de783c23" checksum = "97cbdf2ad6846025e8e25df05171abfb30e3ababa12ee0a0e44b9bbe570633a8"
dependencies = [ dependencies = [
"dtor-proc-macro", "dtor-proc-macro",
] ]
@@ -2179,9 +2179,9 @@ dependencies = [
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.15" version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys", "js-sys",
@@ -2306,7 +2306,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui" name = "gpui"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#107d8ca483276263362c23482612d1cadea81c71" source = "git+https://github.com/zed-industries/zed#6a009b447af415f80cdb55d8c6064e984b899fd5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"as-raw-xcb-connection", "as-raw-xcb-connection",
@@ -2398,7 +2398,7 @@ dependencies = [
[[package]] [[package]]
name = "gpui_macros" name = "gpui_macros"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#107d8ca483276263362c23482612d1cadea81c71" source = "git+https://github.com/zed-industries/zed#6a009b447af415f80cdb55d8c6064e984b899fd5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2622,7 +2622,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client" name = "http_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#107d8ca483276263362c23482612d1cadea81c71" source = "git+https://github.com/zed-industries/zed#6a009b447af415f80cdb55d8c6064e984b899fd5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -2639,7 +2639,7 @@ dependencies = [
[[package]] [[package]]
name = "http_client_tls" name = "http_client_tls"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#107d8ca483276263362c23482612d1cadea81c71" source = "git+https://github.com/zed-industries/zed#6a009b447af415f80cdb55d8c6064e984b899fd5"
dependencies = [ dependencies = [
"rustls", "rustls",
"rustls-platform-verifier", "rustls-platform-verifier",
@@ -3198,14 +3198,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"windows-targets 0.48.5", "windows-targets 0.52.6",
] ]
[[package]] [[package]]
name = "libm" name = "libm"
version = "0.2.11" version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" checksum = "c9627da5196e5d8ed0b0495e61e518847578da83483c37288316d9b2e03a7f72"
[[package]] [[package]]
name = "libredox" name = "libredox"
@@ -3380,7 +3380,7 @@ dependencies = [
[[package]] [[package]]
name = "media" name = "media"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#107d8ca483276263362c23482612d1cadea81c71" source = "git+https://github.com/zed-industries/zed#6a009b447af415f80cdb55d8c6064e984b899fd5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bindgen 0.71.1", "bindgen 0.71.1",
@@ -3508,7 +3508,7 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
dependencies = [ dependencies = [
"getrandom 0.2.15", "getrandom 0.2.16",
] ]
[[package]] [[package]]
@@ -3578,7 +3578,7 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
[[package]] [[package]]
name = "nostr" name = "nostr"
version = "0.41.0" version = "0.41.0"
source = "git+https://github.com/rust-nostr/nostr#54bdb993633146ca4bcfbcedd15735fff7dcbada" source = "git+https://github.com/rust-nostr/nostr#078cfdd7130c94e461ba8f85956ed0a8ae364dac"
dependencies = [ dependencies = [
"aes", "aes",
"base64", "base64",
@@ -3588,7 +3588,7 @@ dependencies = [
"cbc", "cbc",
"chacha20", "chacha20",
"chacha20poly1305", "chacha20poly1305",
"getrandom 0.2.15", "getrandom 0.2.16",
"instant", "instant",
"regex", "regex",
"reqwest 0.12.15", "reqwest 0.12.15",
@@ -3603,7 +3603,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-connect" name = "nostr-connect"
version = "0.41.0" version = "0.41.0"
source = "git+https://github.com/rust-nostr/nostr#54bdb993633146ca4bcfbcedd15735fff7dcbada" source = "git+https://github.com/rust-nostr/nostr#078cfdd7130c94e461ba8f85956ed0a8ae364dac"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -3615,7 +3615,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-database" name = "nostr-database"
version = "0.41.0" version = "0.41.0"
source = "git+https://github.com/rust-nostr/nostr#54bdb993633146ca4bcfbcedd15735fff7dcbada" source = "git+https://github.com/rust-nostr/nostr#078cfdd7130c94e461ba8f85956ed0a8ae364dac"
dependencies = [ dependencies = [
"flatbuffers", "flatbuffers",
"lru", "lru",
@@ -3626,7 +3626,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-lmdb" name = "nostr-lmdb"
version = "0.41.0" version = "0.41.0"
source = "git+https://github.com/rust-nostr/nostr#54bdb993633146ca4bcfbcedd15735fff7dcbada" source = "git+https://github.com/rust-nostr/nostr#078cfdd7130c94e461ba8f85956ed0a8ae364dac"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"heed", "heed",
@@ -3639,7 +3639,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-relay-pool" name = "nostr-relay-pool"
version = "0.41.0" version = "0.41.0"
source = "git+https://github.com/rust-nostr/nostr#54bdb993633146ca4bcfbcedd15735fff7dcbada" source = "git+https://github.com/rust-nostr/nostr#078cfdd7130c94e461ba8f85956ed0a8ae364dac"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"async-wsocket", "async-wsocket",
@@ -3656,7 +3656,7 @@ dependencies = [
[[package]] [[package]]
name = "nostr-sdk" name = "nostr-sdk"
version = "0.41.0" version = "0.41.0"
source = "git+https://github.com/rust-nostr/nostr#54bdb993633146ca4bcfbcedd15735fff7dcbada" source = "git+https://github.com/rust-nostr/nostr#078cfdd7130c94e461ba8f85956ed0a8ae364dac"
dependencies = [ dependencies = [
"async-utility", "async-utility",
"nostr", "nostr",
@@ -4563,9 +4563,9 @@ dependencies = [
[[package]] [[package]]
name = "quinn-proto" name = "quinn-proto"
version = "0.11.10" version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc" checksum = "bcbafbbdbb0f638fe3f35f3c56739f77a8a1d070cb25603226c83339b391472b"
dependencies = [ dependencies = [
"bytes", "bytes",
"getrandom 0.3.2", "getrandom 0.3.2",
@@ -4657,7 +4657,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [ dependencies = [
"getrandom 0.2.15", "getrandom 0.2.16",
] ]
[[package]] [[package]]
@@ -4805,7 +4805,7 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [ dependencies = [
"getrandom 0.2.15", "getrandom 0.2.16",
"libredox", "libredox",
"thiserror 1.0.69", "thiserror 1.0.69",
] ]
@@ -4813,7 +4813,7 @@ dependencies = [
[[package]] [[package]]
name = "refineable" name = "refineable"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#107d8ca483276263362c23482612d1cadea81c71" source = "git+https://github.com/zed-industries/zed#6a009b447af415f80cdb55d8c6064e984b899fd5"
dependencies = [ dependencies = [
"derive_refineable", "derive_refineable",
"workspace-hack", "workspace-hack",
@@ -4951,7 +4951,7 @@ dependencies = [
[[package]] [[package]]
name = "reqwest_client" name = "reqwest_client"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#107d8ca483276263362c23482612d1cadea81c71" source = "git+https://github.com/zed-industries/zed#6a009b447af415f80cdb55d8c6064e984b899fd5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -4998,7 +4998,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
"getrandom 0.2.15", "getrandom 0.2.16",
"libc", "libc",
"untrusted", "untrusted",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@@ -5421,7 +5421,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
[[package]] [[package]]
name = "semantic_version" name = "semantic_version"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#107d8ca483276263362c23482612d1cadea81c71" source = "git+https://github.com/zed-industries/zed#6a009b447af415f80cdb55d8c6064e984b899fd5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"serde", "serde",
@@ -5744,7 +5744,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "sum_tree" name = "sum_tree"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#107d8ca483276263362c23482612d1cadea81c71" source = "git+https://github.com/zed-industries/zed#6a009b447af415f80cdb55d8c6064e984b899fd5"
dependencies = [ dependencies = [
"arrayvec", "arrayvec",
"log", "log",
@@ -6646,7 +6646,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "util" name = "util"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/zed-industries/zed#107d8ca483276263362c23482612d1cadea81c71" source = "git+https://github.com/zed-industries/zed#6a009b447af415f80cdb55d8c6064e984b899fd5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-fs", "async-fs",
@@ -7076,7 +7076,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]

View File

@@ -2,8 +2,9 @@ use account::Account;
use anyhow::Error; use anyhow::Error;
use global::get_client; use global::get_client;
use gpui::{ use gpui::{
div, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis, Context, Entity, div, image_cache, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Axis,
InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription, Task, Window, Context, Entity, InteractiveElement, IntoElement, ParentElement, Render, Styled, Subscription,
Task, Window,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use serde::Deserialize; use serde::Deserialize;
@@ -16,9 +17,14 @@ use ui::{
ContextModal, IconName, Root, Sizable, TitleBar, ContextModal, IconName, Root, Sizable, TitleBar,
}; };
use crate::views::{chat, compose, contacts, login, new_account, profile, relays, welcome}; use crate::{
use crate::views::{onboarding, sidebar}; 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 MODAL_WIDTH: f32 = 420.;
const SIDEBAR_WIDTH: f32 = 280.; const SIDEBAR_WIDTH: f32 = 280.;
@@ -286,6 +292,9 @@ impl Render for ChatSpace {
div() div()
.relative() .relative()
.size_full()
.child(
image_cache(cache_provider("image-cache", CACHE_SIZE))
.size_full() .size_full()
.child( .child(
div() div()
@@ -317,7 +326,8 @@ impl Render for ChatSpace {
this.icon(IconName::Moon) this.icon(IconName::Moon)
} }
}) })
.on_click(cx.listener(|_, _, window, cx| { .on_click(cx.listener(
|_, _, window, cx| {
if cx.theme().appearance.is_dark() { if cx.theme().appearance.is_dark() {
Theme::change( Theme::change(
Appearance::Light, Appearance::Light,
@@ -331,13 +341,15 @@ impl Render for ChatSpace {
cx, cx,
); );
} }
})), },
)),
), ),
), ),
) )
}) })
// Dock // Dock
.child(self.dock.clone()), .child(self.dock.clone()),
),
) )
// Notifications // Notifications
.child(div().absolute().top_8().children(notification_layer)) .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 asset;
pub(crate) mod chatspace; pub(crate) mod chatspace;
pub(crate) mod lru_cache;
pub(crate) mod views; pub(crate) mod views;
actions!(coop, [Quit]); actions!(coop, [Quit]);

View File

@@ -226,7 +226,6 @@ impl Chat {
return; return;
} }
// temporarily disable message input
self.input.update(cx, |this, cx| { self.input.update(cx, |this, cx| {
this.set_loading(true, window, cx); this.set_loading(true, window, cx);
this.set_disabled(true, window, cx); this.set_disabled(true, window, cx);
@@ -235,17 +234,14 @@ impl Chat {
let room = self.room.read(cx); let room = self.room.read(cx);
let task = room.send_message(content, cx); let task = room.send_message(content, cx);
cx.spawn_in(window, async move |this, cx| { cx.spawn_in(window, async move |this, cx| match task.await {
match task.await {
Ok(reports) => { Ok(reports) => {
cx.update(|window, cx| { cx.update(|window, cx| {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
// Reset message input
this.input.update(cx, |this, cx| { this.input.update(cx, |this, cx| {
this.set_loading(false, window, cx); this.set_loading(false, window, cx);
this.set_disabled(false, window, cx); this.set_disabled(false, window, cx);
this.set_text("", window, cx); this.set_text("", window, cx);
cx.notify();
}); });
}) })
.ok(); .ok();
@@ -268,7 +264,6 @@ impl Chat {
}) })
.ok(); .ok();
} }
}
}) })
.detach(); .detach();
} }