Use the user from the `CloudUserStore` to drive the user menu (#35375)

Marshall Bowers created

This PR updates the user menu in the title bar to base the "signed in"
state on the user in the `CloudUserStore` rather than the `UserStore`.

This makes it possible to be signed-in—at least, as far as the user menu
is concerned—even when disconnected from Collab.

Release Notes:

- N/A

Change summary

crates/client/src/client.rs                     |  1 
crates/client/src/cloud/user_store.rs           | 41 ++++++++++++++----
crates/cloud_api_client/src/cloud_api_client.rs |  4 +
crates/title_bar/src/title_bar.rs               | 36 +++++++++-------
4 files changed, 56 insertions(+), 26 deletions(-)

Detailed changes

crates/client/src/client.rs 🔗

@@ -1491,6 +1491,7 @@ impl Client {
 
     pub async fn sign_out(self: &Arc<Self>, cx: &AsyncApp) {
         self.state.write().credentials = None;
+        self.cloud_client.clear_credentials();
         self.disconnect(cx);
 
         if self.has_credentials(cx).await {

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

@@ -7,35 +7,56 @@ use gpui::{Context, Task};
 use util::{ResultExt as _, maybe};
 
 pub struct CloudUserStore {
-    authenticated_user: Option<AuthenticatedUser>,
-    _fetch_authenticated_user_task: Task<()>,
+    authenticated_user: Option<Arc<AuthenticatedUser>>,
+    _maintain_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| {
+            _maintain_authenticated_user_task: cx.spawn(async move |this, cx| {
                 maybe!(async move {
                     loop {
+                        let Some(this) = this.upgrade() else {
+                            return anyhow::Ok(());
+                        };
+
                         if cloud_client.has_credentials() {
-                            break;
+                            if let Some(response) = cloud_client
+                                .get_authenticated_user()
+                                .await
+                                .context("failed to fetch authenticated user")
+                                .log_err()
+                            {
+                                this.update(cx, |this, _cx| {
+                                    this.authenticated_user = Some(Arc::new(response.user));
+                                })
+                                .ok();
+                            }
+                        } else {
+                            this.update(cx, |this, _cx| {
+                                this.authenticated_user = None;
+                            })
+                            .ok();
                         }
 
                         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();
             }),
         }
     }
+
+    pub fn is_authenticated(&self) -> bool {
+        self.authenticated_user.is_some()
+    }
+
+    pub fn authenticated_user(&self) -> Option<Arc<AuthenticatedUser>> {
+        self.authenticated_user.clone()
+    }
 }

crates/cloud_api_client/src/cloud_api_client.rs 🔗

@@ -35,6 +35,10 @@ impl CloudApiClient {
         });
     }
 
+    pub fn clear_credentials(&self) {
+        *self.credentials.write() = None;
+    }
+
     fn authorization_header(&self) -> Result<String> {
         let guard = self.credentials.read();
         let credentials = guard

crates/title_bar/src/title_bar.rs 🔗

@@ -20,7 +20,7 @@ use crate::application_menu::{
 
 use auto_update::AutoUpdateStatus;
 use call::ActiveCall;
-use client::{Client, UserStore, zed_urls};
+use client::{Client, CloudUserStore, UserStore, zed_urls};
 use gpui::{
     Action, AnyElement, App, Context, Corner, Element, Entity, Focusable, InteractiveElement,
     IntoElement, MouseButton, ParentElement, Render, StatefulInteractiveElement, Styled,
@@ -126,6 +126,7 @@ pub struct TitleBar {
     platform_titlebar: Entity<PlatformTitleBar>,
     project: Entity<Project>,
     user_store: Entity<UserStore>,
+    cloud_user_store: Entity<CloudUserStore>,
     client: Arc<Client>,
     workspace: WeakEntity<Workspace>,
     application_menu: Option<Entity<ApplicationMenu>>,
@@ -179,24 +180,25 @@ impl Render for TitleBar {
             children.push(self.banner.clone().into_any_element())
         }
 
+        let is_authenticated = self.cloud_user_store.read(cx).is_authenticated();
+        let status = self.client.status();
+        let status = &*status.borrow();
+
+        let show_sign_in = !is_authenticated || !matches!(status, client::Status::Connected { .. });
+
         children.push(
             h_flex()
                 .gap_1()
                 .pr_1()
                 .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
                 .children(self.render_call_controls(window, cx))
-                .map(|el| {
-                    let status = self.client.status();
-                    let status = &*status.borrow();
-                    if matches!(status, client::Status::Connected { .. }) {
-                        el.child(self.render_user_menu_button(cx))
-                    } else {
-                        el.children(self.render_connection_status(status, cx))
-                            .when(TitleBarSettings::get_global(cx).show_sign_in, |el| {
-                                el.child(self.render_sign_in_button(cx))
-                            })
-                            .child(self.render_user_menu_button(cx))
-                    }
+                .children(self.render_connection_status(status, cx))
+                .when(
+                    show_sign_in && TitleBarSettings::get_global(cx).show_sign_in,
+                    |el| el.child(self.render_sign_in_button(cx)),
+                )
+                .when(is_authenticated, |parent| {
+                    parent.child(self.render_user_menu_button(cx))
                 })
                 .into_any_element(),
         );
@@ -246,6 +248,7 @@ impl TitleBar {
     ) -> Self {
         let project = workspace.project().clone();
         let user_store = workspace.app_state().user_store.clone();
+        let cloud_user_store = workspace.app_state().cloud_user_store.clone();
         let client = workspace.app_state().client.clone();
         let active_call = ActiveCall::global(cx);
 
@@ -293,6 +296,7 @@ impl TitleBar {
             workspace: workspace.weak_handle(),
             project,
             user_store,
+            cloud_user_store,
             client,
             _subscriptions: subscriptions,
             banner,
@@ -628,15 +632,15 @@ impl TitleBar {
     }
 
     pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
-        let user_store = self.user_store.read(cx);
-        if let Some(user) = user_store.current_user() {
+        let cloud_user_store = self.cloud_user_store.read(cx);
+        if let Some(user) = cloud_user_store.authenticated_user() {
             let has_subscription_period = self.user_store.read(cx).subscription_period().is_some();
             let plan = self.user_store.read(cx).current_plan().filter(|_| {
                 // Since the user might be on the legacy free plan we filter based on whether we have a subscription period.
                 has_subscription_period
             });
 
-            let user_avatar = user.avatar_uri.clone();
+            let user_avatar = user.avatar_url.clone();
             let free_chip_bg = cx
                 .theme()
                 .colors()