Skip to content

Commit 342bc5d

Browse files
tmmampagent
andauthored
feat(zbugs-gpui): show avatars (#158)
Amp-Thread-ID: https://ampcode.com/threads/T-cbacad36-05e5-4611-baca-5ce275bfe755 Co-authored-by: Amp <amp@ampcode.com>
1 parent 34e2313 commit 342bc5d

5 files changed

Lines changed: 128 additions & 1 deletion

File tree

‎apps/zbugs-gpui/Cargo.lock‎

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎apps/zbugs-gpui/Cargo.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ publish = false
1111
[workspace]
1212

1313
[dependencies]
14+
anyhow = "1"
1415
# Relative times in the local time zone, as zbugs' `RelativeTime`.
1516
chrono = { version = "0.4", default-features = false, features = ["clock"] }
1617
futures = "0.3"
@@ -36,6 +37,8 @@ tokio-tungstenite = { version = "0.30", default-features = false, features = [
3637
"connect",
3738
"rustls-tls-webpki-roots",
3839
] }
40+
# Avatars: GPUI loads images over HTTP with what the app gives it.
41+
ureq = { version = "3", default-features = false, features = ["rustls"] }
3942
zbugs-model = { path = "../zbugs-model" }
4043
# SQLite storage, so the app keeps its data across launches.
4144
zero-client = { path = "../../rust/zero-client", features = ["sqlite"] }

‎apps/zbugs-gpui/src/app.rs‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,31 @@ struct Seen {
222222
emoji: BTreeSet<String>,
223223
}
224224

225+
/// `user`'s avatar, round, as zbugs' `AvatarImage`; GitHub's at 64
226+
/// pixels, as `avatarURLWithSize` asks for.
227+
fn avatar(user: &User, size: Pixels) -> impl IntoElement {
228+
let url = match http_client::Url::parse(&user.avatar) {
229+
Ok(mut url) if url.host_str() == Some("avatars.githubusercontent.com") => {
230+
let query = url
231+
.query_pairs()
232+
.filter(|(key, _)| key != "size")
233+
.map(|(key, value)| (key.into_owned(), value.into_owned()))
234+
.collect::<Vec<_>>();
235+
url.query_pairs_mut()
236+
.clear()
237+
.extend_pairs(query)
238+
.append_pair("size", "64");
239+
url.to_string()
240+
}
241+
_ => user.avatar.clone(),
242+
};
243+
img(url)
244+
.flex_none()
245+
.size(size)
246+
.rounded_full()
247+
.with_fallback(move || div().size(size).into_any_element())
248+
}
249+
225250
/// What a reaction is on.
226251
#[derive(Clone, Copy)]
227252
enum Subject {
@@ -2338,7 +2363,13 @@ impl ZbugsApp {
23382363
.text_color(cx.theme().muted_foreground)
23392364
.children(issue.short_id.map(|n| format!("#{n}")))
23402365
.child(if issue.open { "Open" } else { "Closed" })
2341-
.child(format!("by {}", who(&issue.creator)))
2366+
.child(
2367+
h_flex()
2368+
.gap_1()
2369+
.child("by")
2370+
.children(issue.creator.as_deref().map(|u| avatar(u, px(18.))))
2371+
.child(who(&issue.creator)),
2372+
)
23422373
.child(format!(
23432374
"updated {}",
23442375
relative_time(issue.modified, now_ms())
@@ -2403,6 +2434,7 @@ impl ZbugsApp {
24032434
.gap_1()
24042435
.text_xs()
24052436
.text_color(cx.theme().muted_foreground)
2437+
.children(c.creator.as_deref().map(|u| avatar(u, px(18.))))
24062438
.child(who(&c.creator))
24072439
.child(div().flex_1().child(relative_time(c.created, now_ms())))
24082440
.when(self.can_change(&c.creator_id), |row| {

‎apps/zbugs-gpui/src/http.rs‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//! The HTTP client GPUI loads images with, for avatars: blocking `ureq`
2+
//! requests on their own threads. GPUI has none of its own.
3+
4+
use futures::FutureExt as _;
5+
use futures::channel::oneshot;
6+
use futures::future::BoxFuture;
7+
use gpui_kit::http_client::http::HeaderValue;
8+
use gpui_kit::http_client::{AsyncBody, HttpClient, Request, Response, Url};
9+
use std::time::Duration;
10+
11+
pub struct UreqClient(ureq::Agent);
12+
13+
impl UreqClient {
14+
pub fn new() -> Self {
15+
let config = ureq::Agent::config_builder()
16+
.timeout_global(Some(Duration::from_secs(30)))
17+
.build();
18+
Self(config.into())
19+
}
20+
}
21+
22+
impl HttpClient for UreqClient {
23+
fn user_agent(&self) -> Option<&HeaderValue> {
24+
None
25+
}
26+
27+
fn proxy(&self) -> Option<&Url> {
28+
None
29+
}
30+
31+
/// GETs `req`'s URL, which is all images need.
32+
fn send(
33+
&self,
34+
req: Request<AsyncBody>,
35+
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
36+
let (agent, uri) = (self.0.clone(), req.uri().to_string());
37+
let (done, response) = oneshot::channel();
38+
std::thread::spawn(move || {
39+
let get = || -> anyhow::Result<Response<AsyncBody>> {
40+
let mut response = agent.get(&uri).call()?;
41+
let body = response.body_mut().read_to_vec()?;
42+
Ok(Response::builder()
43+
.status(response.status())
44+
.body(AsyncBody::from(body))?)
45+
};
46+
let _ = done.send(get());
47+
});
48+
response
49+
.map(|result| result.unwrap_or_else(|_| Err(anyhow::anyhow!("the request ended"))))
50+
.boxed()
51+
}
52+
}

‎apps/zbugs-gpui/src/main.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
//! zbugs on the Zero Rust client, in GPUI.
22
33
mod app;
4+
mod http;
45
mod live;
56
mod login;
67
mod session;
78
mod token;
89
mod transport;
910

1011
use gpui_kit::*;
12+
use std::sync::Arc;
1113

1214
fn main() {
1315
gpui_kit::application()
1416
.with_assets(gpui_kit::assets::Assets)
17+
.with_http_client(Arc::new(http::UreqClient::new()))
1518
.run(|cx| {
1619
gpui_kit::init(cx);
1720
app::bind_keys(cx);

0 commit comments

Comments
 (0)