mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-10 14:06:11 +03:00
Introduce HttpClient trait, use it to fetch avatars in UserStore
* Add a FakeHttpClient for tests
This commit is contained in:
parent
f0019e3725
commit
84d4bb6186
@ -1,8 +1,11 @@
|
||||
use crate::geometry::vector::{vec2i, Vector2I};
|
||||
use image::{Bgra, ImageBuffer};
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
use std::{
|
||||
fmt,
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct ImageData {
|
||||
@ -29,3 +32,12 @@ impl ImageData {
|
||||
vec2i(width as i32, height as i32)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ImageData {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("ImageData")
|
||||
.field("id", &self.id)
|
||||
.field("size", &self.data.dimensions())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
@ -1025,6 +1025,7 @@ mod tests {
|
||||
language::LanguageRegistry,
|
||||
rpc::{self, Client},
|
||||
settings,
|
||||
test::FakeHttpClient,
|
||||
user::UserStore,
|
||||
worktree::Worktree,
|
||||
};
|
||||
@ -1486,6 +1487,7 @@ mod tests {
|
||||
|
||||
// Connect to a server as 2 clients.
|
||||
let mut server = TestServer::start().await;
|
||||
let mut http = FakeHttpClient::new(|_| async move { Ok(Response::new(404)) });
|
||||
let (user_id_a, client_a) = server.create_client(&mut cx_a, "user_a").await;
|
||||
let (user_id_b, client_b) = server.create_client(&mut cx_b, "user_b").await;
|
||||
|
||||
|
@ -46,7 +46,7 @@ pub struct Channel {
|
||||
_subscription: rpc::Subscription,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ChannelMessage {
|
||||
pub id: u64,
|
||||
pub body: String,
|
||||
@ -495,15 +495,17 @@ impl<'a> sum_tree::SeekDimension<'a, ChannelMessageSummary> for Count {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::FakeServer;
|
||||
use crate::test::{FakeHttpClient, FakeServer};
|
||||
use gpui::TestAppContext;
|
||||
use surf::http::Response;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_channel_messages(mut cx: TestAppContext) {
|
||||
let user_id = 5;
|
||||
let mut client = Client::new();
|
||||
let http_client = FakeHttpClient::new(|_| async move { Ok(Response::new(404)) });
|
||||
let server = FakeServer::for_client(user_id, &mut client, &cx).await;
|
||||
let user_store = UserStore::new(client.clone(), cx.background().as_ref());
|
||||
let user_store = UserStore::new(client.clone(), http_client, cx.background().as_ref());
|
||||
|
||||
let channel_list = cx.add_model(|cx| ChannelList::new(user_store, client.clone(), cx));
|
||||
channel_list.read_with(&cx, |list, _| assert_eq!(list.available_channels(), None));
|
||||
|
26
zed/src/http.rs
Normal file
26
zed/src/http.rs
Normal file
@ -0,0 +1,26 @@
|
||||
pub use anyhow::{anyhow, Result};
|
||||
use futures::future::BoxFuture;
|
||||
use std::sync::Arc;
|
||||
pub use surf::{
|
||||
http::{Method, Request, Response as ServerResponse},
|
||||
Response, Url,
|
||||
};
|
||||
|
||||
pub trait HttpClient: Send + Sync {
|
||||
fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result<Response>>;
|
||||
}
|
||||
|
||||
pub fn client() -> Arc<dyn HttpClient> {
|
||||
Arc::new(surf::client())
|
||||
}
|
||||
|
||||
impl HttpClient for surf::Client {
|
||||
fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result<Response>> {
|
||||
Box::pin(async move {
|
||||
Ok(self
|
||||
.send(req)
|
||||
.await
|
||||
.map_err(|e| anyhow!("http request failed: {}", e))?)
|
||||
})
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ pub mod editor;
|
||||
pub mod file_finder;
|
||||
pub mod fs;
|
||||
mod fuzzy;
|
||||
pub mod http;
|
||||
pub mod language;
|
||||
pub mod menus;
|
||||
pub mod project_browser;
|
||||
|
@ -13,7 +13,7 @@ use zed::{
|
||||
channel::ChannelList,
|
||||
chat_panel, editor, file_finder,
|
||||
fs::RealFs,
|
||||
language, menus, rpc, settings, theme_selector,
|
||||
http, language, menus, rpc, settings, theme_selector,
|
||||
user::UserStore,
|
||||
workspace::{self, OpenNew, OpenParams, OpenPaths},
|
||||
AppState,
|
||||
@ -37,7 +37,8 @@ fn main() {
|
||||
|
||||
app.run(move |cx| {
|
||||
let rpc = rpc::Client::new();
|
||||
let user_store = UserStore::new(rpc.clone(), cx.background());
|
||||
let http = http::client();
|
||||
let user_store = UserStore::new(rpc.clone(), http.clone(), cx.background());
|
||||
let app_state = Arc::new(AppState {
|
||||
languages: languages.clone(),
|
||||
settings_tx: Arc::new(Mutex::new(settings_tx)),
|
||||
|
@ -2,6 +2,7 @@ use crate::{
|
||||
assets::Assets,
|
||||
channel::ChannelList,
|
||||
fs::RealFs,
|
||||
http::{HttpClient, Request, Response, ServerResponse},
|
||||
language::LanguageRegistry,
|
||||
rpc::{self, Client},
|
||||
settings::{self, ThemeRegistry},
|
||||
@ -10,11 +11,13 @@ use crate::{
|
||||
AppState,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::{future::BoxFuture, Future};
|
||||
use gpui::{AsyncAppContext, Entity, ModelHandle, MutableAppContext, TestAppContext};
|
||||
use parking_lot::Mutex;
|
||||
use postage::{mpsc, prelude::Stream as _};
|
||||
use smol::channel;
|
||||
use std::{
|
||||
fmt,
|
||||
marker::PhantomData,
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
@ -164,7 +167,8 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
|
||||
let languages = Arc::new(LanguageRegistry::new());
|
||||
let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
|
||||
let rpc = rpc::Client::new();
|
||||
let user_store = UserStore::new(rpc.clone(), cx.background());
|
||||
let http = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
|
||||
let user_store = UserStore::new(rpc.clone(), http, cx.background());
|
||||
Arc::new(AppState {
|
||||
settings_tx: Arc::new(Mutex::new(settings_tx)),
|
||||
settings,
|
||||
@ -313,3 +317,33 @@ impl FakeServer {
|
||||
self.connection_id.lock().expect("not connected")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FakeHttpClient {
|
||||
handler:
|
||||
Box<dyn 'static + Send + Sync + Fn(Request) -> BoxFuture<'static, Result<ServerResponse>>>,
|
||||
}
|
||||
|
||||
impl FakeHttpClient {
|
||||
pub fn new<Fut, F>(handler: F) -> Arc<dyn HttpClient>
|
||||
where
|
||||
Fut: 'static + Send + Future<Output = Result<ServerResponse>>,
|
||||
F: 'static + Send + Sync + Fn(Request) -> Fut,
|
||||
{
|
||||
Arc::new(Self {
|
||||
handler: Box::new(move |req| Box::pin(handler(req))),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for FakeHttpClient {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("FakeHttpClient").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClient for FakeHttpClient {
|
||||
fn send<'a>(&'a self, req: Request) -> BoxFuture<'a, Result<Response>> {
|
||||
let future = (self.handler)(req);
|
||||
Box::pin(async move { future.await.map(Into::into) })
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,24 @@
|
||||
use crate::{
|
||||
http::{HttpClient, Method, Request, Url},
|
||||
rpc::{Client, Status},
|
||||
util::TryFutureExt,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use gpui::{elements::Image, executor, ImageData, Task};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use futures::future;
|
||||
use gpui::{executor, ImageData, Task};
|
||||
use parking_lot::Mutex;
|
||||
use postage::{prelude::Stream, sink::Sink, watch};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use surf::{
|
||||
http::{Method, Request},
|
||||
HttpClient, Url,
|
||||
use postage::{oneshot, prelude::Stream, sink::Sink, watch};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Weak},
|
||||
};
|
||||
use zrpc::proto;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct User {
|
||||
id: u64,
|
||||
github_login: String,
|
||||
avatar: Option<ImageData>,
|
||||
pub id: u64,
|
||||
pub github_login: String,
|
||||
pub avatar: Option<Arc<ImageData>>,
|
||||
}
|
||||
|
||||
pub struct UserStore {
|
||||
@ -24,7 +26,7 @@ pub struct UserStore {
|
||||
current_user: watch::Receiver<Option<Arc<User>>>,
|
||||
rpc: Arc<Client>,
|
||||
http: Arc<dyn HttpClient>,
|
||||
_maintain_current_user: Option<Task<()>>,
|
||||
_maintain_current_user: Task<()>,
|
||||
}
|
||||
|
||||
impl UserStore {
|
||||
@ -34,18 +36,18 @@ impl UserStore {
|
||||
executor: &executor::Background,
|
||||
) -> Arc<Self> {
|
||||
let (mut current_user_tx, current_user_rx) = watch::channel();
|
||||
|
||||
let mut this = Arc::new(Self {
|
||||
let (mut this_tx, mut this_rx) = oneshot::channel::<Weak<Self>>();
|
||||
let this = Arc::new(Self {
|
||||
users: Default::default(),
|
||||
current_user: current_user_rx,
|
||||
rpc: rpc.clone(),
|
||||
http,
|
||||
_maintain_current_user: None,
|
||||
});
|
||||
|
||||
let task = {
|
||||
let this = Arc::downgrade(&this);
|
||||
executor.spawn(async move {
|
||||
_maintain_current_user: executor.spawn(async move {
|
||||
let this = if let Some(this) = this_rx.recv().await {
|
||||
this
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
let mut status = rpc.status();
|
||||
while let Some(status) = status.recv().await {
|
||||
match status {
|
||||
@ -63,10 +65,12 @@ impl UserStore {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
Arc::get_mut(&mut this).unwrap()._maintain_current_user = Some(task);
|
||||
|
||||
}),
|
||||
});
|
||||
let weak = Arc::downgrade(&this);
|
||||
executor
|
||||
.spawn(async move { this_tx.send(weak).await })
|
||||
.detach();
|
||||
this
|
||||
}
|
||||
|
||||
@ -78,8 +82,15 @@ impl UserStore {
|
||||
|
||||
if !user_ids.is_empty() {
|
||||
let response = self.rpc.request(proto::GetUsers { user_ids }).await?;
|
||||
let new_users = future::join_all(
|
||||
response
|
||||
.users
|
||||
.into_iter()
|
||||
.map(|user| User::new(user, self.http.as_ref())),
|
||||
)
|
||||
.await;
|
||||
let mut users = self.users.lock();
|
||||
for user in response.users {
|
||||
for user in new_users {
|
||||
users.insert(user.id, Arc::new(user));
|
||||
}
|
||||
}
|
||||
@ -92,20 +103,12 @@ impl UserStore {
|
||||
return Ok(user);
|
||||
}
|
||||
|
||||
let response = self
|
||||
.rpc
|
||||
.request(proto::GetUsers {
|
||||
user_ids: vec![user_id],
|
||||
})
|
||||
.await?;
|
||||
|
||||
if let Some(user) = response.users.into_iter().next() {
|
||||
let user = Arc::new(user);
|
||||
self.users.lock().insert(user_id, user.clone());
|
||||
Ok(user)
|
||||
} else {
|
||||
Err(anyhow!("server responded with no users"))
|
||||
}
|
||||
self.load_users(vec![user_id]).await?;
|
||||
self.users
|
||||
.lock()
|
||||
.get(&user_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| anyhow!("server responded with no users"))
|
||||
}
|
||||
|
||||
pub fn current_user(&self) -> &watch::Receiver<Option<Arc<User>>> {
|
||||
@ -115,20 +118,25 @@ impl UserStore {
|
||||
|
||||
impl User {
|
||||
async fn new(message: proto::User, http: &dyn HttpClient) -> Self {
|
||||
let avatar = fetch_avatar(http, &message.avatar_url).await.log_err();
|
||||
User {
|
||||
id: message.id,
|
||||
github_login: message.github_login,
|
||||
avatar,
|
||||
avatar: fetch_avatar(http, &message.avatar_url).log_err().await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
|
||||
let url = Url::parse(url)?;
|
||||
let url = Url::parse(url).with_context(|| format!("failed to parse avatar url {:?}", url))?;
|
||||
let request = Request::new(Method::Get, url);
|
||||
let response = http.send(request).await?;
|
||||
let bytes = response.body_bytes().await?;
|
||||
let mut response = http
|
||||
.send(request)
|
||||
.await
|
||||
.map_err(|e| anyhow!("failed to send user avatar request: {}", e))?;
|
||||
let bytes = response
|
||||
.body_bytes()
|
||||
.await
|
||||
.map_err(|e| anyhow!("failed to read user avatar response body: {}", e))?;
|
||||
let format = image::guess_format(&bytes)?;
|
||||
let image = image::load_from_memory_with_format(&bytes, format)?.into_bgra8();
|
||||
Ok(ImageData::new(image))
|
||||
|
@ -956,8 +956,14 @@ impl Workspace {
|
||||
|
||||
fn render_current_user(&self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = &self.settings.borrow().theme;
|
||||
let avatar = if let Some(current_user) = self.user_store.current_user().borrow().as_ref() {
|
||||
todo!()
|
||||
let avatar = if let Some(avatar) = self
|
||||
.user_store
|
||||
.current_user()
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.and_then(|user| user.avatar.clone())
|
||||
{
|
||||
Image::new(avatar).boxed()
|
||||
} else {
|
||||
Svg::new("icons/signed-out-12.svg")
|
||||
.with_color(theme.workspace.titlebar.icon_signed_out)
|
||||
|
Loading…
Reference in New Issue
Block a user