client: Add CloudUserStore (#35370)

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

Release Notes:

- N/A
This commit is contained in:
Marshall Bowers 2025-07-30 18:43:10 -04:00
parent 35ea2acd1c
commit 03693498d6
7 changed files with 68 additions and 4 deletions

View file

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

View file

@ -0,0 +1,3 @@
mod user_store;
pub use user_store::*;

View file

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

View file

@ -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) { pub fn set_credentials(&self, user_id: u32, access_token: String) {
*self.credentials.write() = Some(Credentials { *self.credentials.write() = Some(Credentials {
user_id, 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() let request = Request::builder()
.method(Method::GET) .method(Method::GET)
.uri( .uri(
@ -69,8 +73,7 @@ impl CloudApiClient {
let mut body = String::new(); let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?; 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)?)
} }
} }

View file

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

View file

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

View file

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