Detailed changes
@@ -2282,6 +2282,16 @@ impl<'a, T: View> ViewContext<'a, T> {
let handle = self.handle();
self.app.spawn(|cx| f(handle, cx))
}
+
+ pub fn spawn_weak<F, Fut, S>(&self, f: F) -> Task<S>
+ where
+ F: FnOnce(WeakViewHandle<T>, AsyncAppContext) -> Fut,
+ Fut: 'static + Future<Output = S>,
+ S: 'static,
+ {
+ let handle = self.handle().downgrade();
+ self.app.spawn(|cx| f(handle, cx))
+ }
}
pub struct RenderContext<'a, T: View> {
@@ -1512,7 +1512,7 @@ mod tests {
.await
.unwrap();
- let user_store_a = Arc::new(UserStore::new(client_a.clone()));
+ let user_store_a = UserStore::new(client_a.clone(), cx_a.background().as_ref());
let channels_a = cx_a.add_model(|cx| ChannelList::new(user_store_a, client_a, cx));
channels_a
.condition(&mut cx_a, |list, _| list.available_channels().is_some())
@@ -1537,7 +1537,7 @@ mod tests {
})
.await;
- let user_store_b = Arc::new(UserStore::new(client_b.clone()));
+ let user_store_b = UserStore::new(client_b.clone(), cx_b.background().as_ref());
let channels_b = cx_b.add_model(|cx| ChannelList::new(user_store_b, client_b, cx));
channels_b
.condition(&mut cx_b, |list, _| list.available_channels().is_some())
@@ -1637,7 +1637,7 @@ mod tests {
.await
.unwrap();
- let user_store_a = Arc::new(UserStore::new(client_a.clone()));
+ let user_store_a = UserStore::new(client_a.clone(), cx_a.background().as_ref());
let channels_a = cx_a.add_model(|cx| ChannelList::new(user_store_a, client_a, cx));
channels_a
.condition(&mut cx_a, |list, _| list.available_channels().is_some())
@@ -1713,7 +1713,7 @@ mod tests {
.await
.unwrap();
- let user_store_a = Arc::new(UserStore::new(client_a.clone()));
+ let user_store_a = UserStore::new(client_a.clone(), cx_a.background().as_ref());
let channels_a = cx_a.add_model(|cx| ChannelList::new(user_store_a, client_a, cx));
channels_a
.condition(&mut cx_a, |list, _| list.available_channels().is_some())
@@ -1739,7 +1739,7 @@ mod tests {
})
.await;
- let user_store_b = Arc::new(UserStore::new(client_b.clone()));
+ let user_store_b = UserStore::new(client_b.clone(), cx_b.background().as_ref());
let channels_b = cx_b.add_model(|cx| ChannelList::new(user_store_b, client_b, cx));
channels_b
.condition(&mut cx_b, |list, _| list.available_channels().is_some())
@@ -118,7 +118,7 @@ impl ChannelList {
cx.notify();
});
}
- rpc::Status::Disconnected { .. } => {
+ rpc::Status::SignedOut { .. } => {
this.update(&mut cx, |this, cx| {
this.available_channels = None;
this.channels.clear();
@@ -503,7 +503,7 @@ mod tests {
let user_id = 5;
let mut client = Client::new();
let server = FakeServer::for_client(user_id, &mut client, &cx).await;
- let user_store = Arc::new(UserStore::new(client.clone()));
+ let user_store = UserStore::new(client.clone(), 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));
@@ -37,7 +37,7 @@ fn main() {
app.run(move |cx| {
let rpc = rpc::Client::new();
- let user_store = Arc::new(UserStore::new(rpc.clone()));
+ let user_store = UserStore::new(rpc.clone(), cx.background());
let app_state = Arc::new(AppState {
languages: languages.clone(),
settings_tx: Arc::new(Mutex::new(settings_tx)),
@@ -39,7 +39,7 @@ pub struct Client {
#[derive(Copy, Clone, Debug)]
pub enum Status {
- Disconnected,
+ SignedOut,
Authenticating,
Connecting {
user_id: u64,
@@ -73,7 +73,7 @@ struct ClientState {
impl Default for ClientState {
fn default() -> Self {
Self {
- status: watch::channel_with(Status::Disconnected),
+ status: watch::channel_with(Status::SignedOut),
entity_id_extractors: Default::default(),
model_handlers: Default::default(),
_maintain_connection: None,
@@ -167,7 +167,7 @@ impl Client {
}
}));
}
- Status::Disconnected => {
+ Status::SignedOut => {
state._maintain_connection.take();
}
_ => {}
@@ -232,7 +232,7 @@ impl Client {
cx: &AsyncAppContext,
) -> anyhow::Result<()> {
let was_disconnected = match *self.status().borrow() {
- Status::Disconnected => true,
+ Status::SignedOut => true,
Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => {
false
}
@@ -324,7 +324,7 @@ impl Client {
cx.foreground()
.spawn(async move {
match handle_io.await {
- Ok(()) => this.set_status(Status::Disconnected, &cx),
+ Ok(()) => this.set_status(Status::SignedOut, &cx),
Err(err) => {
log::error!("connection error: {:?}", err);
this.set_status(Status::ConnectionLost, &cx);
@@ -470,7 +470,7 @@ impl Client {
pub async fn disconnect(self: &Arc<Self>, cx: &AsyncAppContext) -> Result<()> {
let conn_id = self.connection_id()?;
self.peer.disconnect(conn_id).await;
- self.set_status(Status::Disconnected, cx);
+ self.set_status(Status::SignedOut, cx);
Ok(())
}
@@ -164,7 +164,7 @@ 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 = Arc::new(UserStore::new(rpc.clone()));
+ let user_store = UserStore::new(rpc.clone(), cx.background());
Arc::new(AppState {
settings_tx: Arc::new(Mutex::new(settings_tx)),
settings,
@@ -1,22 +1,73 @@
-use crate::rpc::Client;
+use crate::{
+ rpc::{Client, Status},
+ util::TryFutureExt,
+};
use anyhow::{anyhow, Result};
+use gpui::{elements::Image, 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 zrpc::proto;
-pub use proto::User;
+pub struct User {
+ id: u64,
+ github_login: String,
+ avatar: Option<ImageData>,
+}
pub struct UserStore {
users: Mutex<HashMap<u64, Arc<User>>>,
+ current_user: watch::Receiver<Option<Arc<User>>>,
rpc: Arc<Client>,
+ http: Arc<dyn HttpClient>,
+ _maintain_current_user: Option<Task<()>>,
}
impl UserStore {
- pub fn new(rpc: Arc<Client>) -> Self {
- Self {
+ pub fn new(
+ rpc: Arc<Client>,
+ http: Arc<dyn HttpClient>,
+ executor: &executor::Background,
+ ) -> Arc<Self> {
+ let (mut current_user_tx, current_user_rx) = watch::channel();
+
+ let mut this = Arc::new(Self {
users: Default::default(),
- rpc,
- }
+ current_user: current_user_rx,
+ rpc: rpc.clone(),
+ http,
+ _maintain_current_user: None,
+ });
+
+ let task = {
+ let this = Arc::downgrade(&this);
+ executor.spawn(async move {
+ let mut status = rpc.status();
+ while let Some(status) = status.recv().await {
+ match status {
+ Status::Connected { user_id, .. } => {
+ if let Some(this) = this.upgrade() {
+ current_user_tx
+ .send(this.fetch_user(user_id).log_err().await)
+ .await
+ .ok();
+ }
+ }
+ Status::SignedOut => {
+ current_user_tx.send(None).await.ok();
+ }
+ _ => {}
+ }
+ }
+ })
+ };
+ Arc::get_mut(&mut this).unwrap()._maintain_current_user = Some(task);
+
+ this
}
pub async fn load_users(&self, mut user_ids: Vec<u64>) -> Result<()> {
@@ -56,4 +107,29 @@ impl UserStore {
Err(anyhow!("server responded with no users"))
}
}
+
+ pub fn current_user(&self) -> &watch::Receiver<Option<Arc<User>>> {
+ &self.current_user
+ }
+}
+
+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,
+ }
+ }
+}
+
+async fn fetch_avatar(http: &dyn HttpClient, url: &str) -> Result<Arc<ImageData>> {
+ let url = Url::parse(url)?;
+ let request = Request::new(Method::Get, url);
+ let response = http.send(request).await?;
+ let bytes = response.body_bytes().await?;
+ let format = image::guess_format(&bytes)?;
+ let image = image::load_from_memory_with_format(&bytes, format)?.into_bgra8();
+ Ok(ImageData::new(image))
}
@@ -29,7 +29,7 @@ use gpui::{
use log::error;
pub use pane::*;
pub use pane_group::*;
-use postage::watch;
+use postage::{prelude::Stream, watch};
use sidebar::{Side, Sidebar, ToggleSidebarItem};
use smol::prelude::*;
use std::{
@@ -356,6 +356,7 @@ pub struct Workspace {
(usize, Arc<Path>),
postage::watch::Receiver<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
>,
+ _observe_current_user: Task<()>,
}
impl Workspace {
@@ -389,6 +390,18 @@ impl Workspace {
);
right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into());
+ let mut current_user = app_state.user_store.current_user().clone();
+ let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
+ current_user.recv().await;
+ while current_user.recv().await.is_some() {
+ cx.update(|cx| {
+ if let Some(this) = this.upgrade(&cx) {
+ this.update(cx, |_, cx| cx.notify());
+ }
+ })
+ }
+ });
+
Workspace {
modal: None,
center: PaneGroup::new(pane.id()),
@@ -404,6 +417,7 @@ impl Workspace {
worktrees: Default::default(),
items: Default::default(),
loading_items: Default::default(),
+ _observe_current_user,
}
}
@@ -940,17 +954,21 @@ impl Workspace {
&self.active_pane
}
- fn render_account_status(&self, cx: &mut RenderContext<Self>) -> ElementBox {
+ 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!()
+ } else {
+ Svg::new("icons/signed-out-12.svg")
+ .with_color(theme.workspace.titlebar.icon_signed_out)
+ .boxed()
+ };
+
ConstrainedBox::new(
Align::new(
- ConstrainedBox::new(
- Svg::new("icons/signed-out-12.svg")
- .with_color(theme.workspace.titlebar.icon_signed_out)
- .boxed(),
- )
- .with_width(theme.workspace.titlebar.icon_width)
- .boxed(),
+ ConstrainedBox::new(avatar)
+ .with_width(theme.workspace.titlebar.icon_width)
+ .boxed(),
)
.boxed(),
)
@@ -988,7 +1006,7 @@ impl View for Workspace {
.boxed(),
)
.with_child(
- Align::new(self.render_account_status(cx)).right().boxed(),
+ Align::new(self.render_current_user(cx)).right().boxed(),
)
.boxed(),
)