WIP

Nathan Sobo created

Change summary

gpui/src/app.rs      | 10 +++++
server/src/rpc.rs    | 10 ++--
zed/src/channel.rs   |  4 +-
zed/src/main.rs      |  2 
zed/src/rpc.rs       | 12 +++---
zed/src/test.rs      |  2 
zed/src/user.rs      | 88 ++++++++++++++++++++++++++++++++++++++++++---
zed/src/workspace.rs | 38 ++++++++++++++-----
8 files changed, 135 insertions(+), 31 deletions(-)

Detailed changes

gpui/src/app.rs 🔗

@@ -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> {

server/src/rpc.rs 🔗

@@ -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())

zed/src/channel.rs 🔗

@@ -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));

zed/src/main.rs 🔗

@@ -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)),

zed/src/rpc.rs 🔗

@@ -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(())
     }
 

zed/src/test.rs 🔗

@@ -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,

zed/src/user.rs 🔗

@@ -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))
 }

zed/src/workspace.rs 🔗

@@ -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(),
                         )