client: Add `CloudUserStore` (#35370)

Marshall Bowers created

This PR adds a new `CloudUserStore` for storing information about the
user retrieved from Cloud instead of Collab.

Release Notes:

- N/A

Change summary

crates/client/src/client.rs                     |  6 ++
crates/client/src/cloud.rs                      |  3 +
crates/client/src/cloud/user_store.rs           | 41 +++++++++++++++++++
crates/cloud_api_client/src/cloud_api_client.rs |  9 ++-
crates/collab/src/tests/test_server.rs          |  3 +
crates/workspace/src/workspace.rs               |  6 ++
crates/zed/src/main.rs                          |  4 +
7 files changed, 68 insertions(+), 4 deletions(-)

Detailed changes

crates/client/src/client.rs 🔗

@@ -1,6 +1,7 @@
 #[cfg(any(test, feature = "test-support"))]
 pub mod test;
 
+mod cloud;
 mod proxy;
 pub mod telemetry;
 pub mod user;
@@ -52,6 +53,7 @@ use tokio::net::TcpStream;
 use url::Url;
 use util::{ConnectionResult, ResultExt};
 
+pub use cloud::*;
 pub use rpc::*;
 pub use telemetry_events::Event;
 pub use user::*;
@@ -621,6 +623,10 @@ impl Client {
         self.http.clone()
     }
 
+    pub fn cloud_client(&self) -> Arc<CloudApiClient> {
+        self.cloud_client.clone()
+    }
+
     pub fn set_id(&self, id: u64) -> &Self {
         self.id.store(id, Ordering::SeqCst);
         self

crates/client/src/cloud/user_store.rs 🔗

@@ -0,0 +1,41 @@
+use std::sync::Arc;
+use std::time::Duration;
+
+use anyhow::Context as _;
+use cloud_api_client::{AuthenticatedUser, CloudApiClient};
+use gpui::{Context, Task};
+use util::{ResultExt as _, maybe};
+
+pub struct CloudUserStore {
+    authenticated_user: Option<AuthenticatedUser>,
+    _fetch_authenticated_user_task: Task<()>,
+}
+
+impl CloudUserStore {
+    pub fn new(cloud_client: Arc<CloudApiClient>, cx: &mut Context<Self>) -> Self {
+        Self {
+            authenticated_user: None,
+            _fetch_authenticated_user_task: cx.spawn(async move |this, cx| {
+                maybe!(async move {
+                    loop {
+                        if cloud_client.has_credentials() {
+                            break;
+                        }
+
+                        cx.background_executor()
+                            .timer(Duration::from_millis(100))
+                            .await;
+                    }
+
+                    let response = cloud_client.get_authenticated_user().await?;
+                    this.update(cx, |this, _cx| {
+                        this.authenticated_user = Some(response.user);
+                    })
+                })
+                .await
+                .context("failed to fetch authenticated user")
+                .log_err();
+            }),
+        }
+    }
+}

crates/cloud_api_client/src/cloud_api_client.rs 🔗

@@ -24,6 +24,10 @@ impl CloudApiClient {
         }
     }
 
+    pub fn has_credentials(&self) -> bool {
+        self.credentials.read().is_some()
+    }
+
     pub fn set_credentials(&self, user_id: u32, access_token: String) {
         *self.credentials.write() = Some(Credentials {
             user_id,
@@ -43,7 +47,7 @@ impl CloudApiClient {
         ))
     }
 
-    pub async fn get_authenticated_user(&self) -> Result<AuthenticatedUser> {
+    pub async fn get_authenticated_user(&self) -> Result<GetAuthenticatedUserResponse> {
         let request = Request::builder()
             .method(Method::GET)
             .uri(
@@ -69,8 +73,7 @@ impl CloudApiClient {
 
         let mut body = String::new();
         response.body_mut().read_to_string(&mut body).await?;
-        let response: GetAuthenticatedUserResponse = serde_json::from_str(&body)?;
 
-        Ok(response.user)
+        Ok(serde_json::from_str(&body)?)
     }
 }

crates/collab/src/tests/test_server.rs 🔗

@@ -8,6 +8,7 @@ use crate::{
 use anyhow::anyhow;
 use call::ActiveCall;
 use channel::{ChannelBuffer, ChannelStore};
+use client::CloudUserStore;
 use client::{
     self, ChannelId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
     proto::PeerId,
@@ -281,12 +282,14 @@ impl TestServer {
             .register_hosting_provider(Arc::new(git_hosting_providers::Github::public_instance()));
 
         let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
+        let cloud_user_store = cx.new(|cx| CloudUserStore::new(client.cloud_client(), cx));
         let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
         let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
         let session = cx.new(|cx| AppSession::new(Session::test(), cx));
         let app_state = Arc::new(workspace::AppState {
             client: client.clone(),
             user_store: user_store.clone(),
+            cloud_user_store,
             workspace_store,
             languages: language_registry,
             fs: fs.clone(),

crates/workspace/src/workspace.rs 🔗

@@ -15,6 +15,7 @@ mod toast_layer;
 mod toolbar;
 mod workspace_settings;
 
+use client::CloudUserStore;
 pub use toast_layer::{ToastAction, ToastLayer, ToastView};
 
 use anyhow::{Context as _, Result, anyhow};
@@ -839,6 +840,7 @@ pub struct AppState {
     pub languages: Arc<LanguageRegistry>,
     pub client: Arc<Client>,
     pub user_store: Entity<UserStore>,
+    pub cloud_user_store: Entity<CloudUserStore>,
     pub workspace_store: Entity<WorkspaceStore>,
     pub fs: Arc<dyn fs::Fs>,
     pub build_window_options: fn(Option<Uuid>, &mut App) -> WindowOptions,
@@ -911,6 +913,7 @@ impl AppState {
         let client = Client::new(clock, http_client.clone(), cx);
         let session = cx.new(|cx| AppSession::new(Session::test(), cx));
         let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
+        let cloud_user_store = cx.new(|cx| CloudUserStore::new(client.cloud_client(), cx));
         let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
 
         theme::init(theme::LoadThemes::JustBase, cx);
@@ -922,6 +925,7 @@ impl AppState {
             fs,
             languages,
             user_store,
+            cloud_user_store,
             workspace_store,
             node_runtime: NodeRuntime::unavailable(),
             build_window_options: |_, _| Default::default(),
@@ -5689,6 +5693,7 @@ impl Workspace {
 
         let client = project.read(cx).client();
         let user_store = project.read(cx).user_store();
+        let cloud_user_store = cx.new(|cx| CloudUserStore::new(client.cloud_client(), cx));
 
         let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
         let session = cx.new(|cx| AppSession::new(Session::test(), cx));
@@ -5696,6 +5701,7 @@ impl Workspace {
         let app_state = Arc::new(AppState {
             languages: project.read(cx).languages().clone(),
             workspace_store,
+            cloud_user_store,
             client,
             user_store,
             fs: project.read(cx).fs().clone(),

crates/zed/src/main.rs 🔗

@@ -5,7 +5,7 @@ use agent_ui::AgentPanel;
 use anyhow::{Context as _, Result};
 use clap::{Parser, command};
 use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
-use client::{Client, ProxySettings, UserStore, parse_zed_link};
+use client::{Client, CloudUserStore, ProxySettings, UserStore, parse_zed_link};
 use collab_ui::channel_view::ChannelView;
 use collections::HashMap;
 use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE};
@@ -457,6 +457,7 @@ pub fn main() {
         language::init(cx);
         languages::init(languages.clone(), node_runtime.clone(), cx);
         let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
+        let cloud_user_store = cx.new(|cx| CloudUserStore::new(client.cloud_client(), cx));
         let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
 
         language_extension::init(
@@ -516,6 +517,7 @@ pub fn main() {
             languages: languages.clone(),
             client: client.clone(),
             user_store: user_store.clone(),
+            cloud_user_store,
             fs: fs.clone(),
             build_window_options,
             workspace_store,