Detailed changes
@@ -1949,6 +1949,7 @@ name = "collab_ui2"
version = "0.1.0"
dependencies = [
"anyhow",
+ "auto_update2",
"call2",
"channel2",
"client2",
@@ -1957,6 +1958,7 @@ dependencies = [
"db2",
"editor2",
"feature_flags2",
+ "feedback2",
"futures 0.3.28",
"fuzzy2",
"gpui2",
@@ -1978,6 +1980,7 @@ dependencies = [
"settings2",
"smallvec",
"theme2",
+ "theme_selector2",
"time",
"tree-sitter-markdown",
"ui2",
@@ -6,7 +6,7 @@ use db::kvp::KEY_VALUE_STORE;
use db::RELEASE_CHANNEL;
use gpui::{
actions, AppContext, AsyncAppContext, Context as _, Model, ModelContext, SemanticVersion, Task,
- ViewContext, VisualContext,
+ ViewContext, VisualContext, WindowContext,
};
use isahc::AsyncBody;
@@ -118,7 +118,7 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
}
}
-pub fn check(_: &Check, cx: &mut ViewContext<Workspace>) {
+pub fn check(_: &Check, cx: &mut WindowContext) {
if let Some(updater) = AutoUpdater::get(cx) {
updater.update(cx, |updater, cx| updater.poll(cx));
} else {
@@ -11,8 +11,8 @@ use async_tungstenite::tungstenite::{
http::{Request, StatusCode},
};
use futures::{
- future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryFutureExt as _,
- TryStreamExt,
+ channel::oneshot, future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt,
+ TryFutureExt as _, TryStreamExt,
};
use gpui::{
actions, serde_json, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Model,
@@ -1020,91 +1020,116 @@ impl Client {
) -> Task<Result<Credentials>> {
let http = self.http.clone();
cx.spawn(|cx| async move {
- // Generate a pair of asymmetric encryption keys. The public key will be used by the
- // zed server to encrypt the user's access token, so that it can'be intercepted by
- // any other app running on the user's device.
- let (public_key, private_key) =
- rpc::auth::keypair().expect("failed to generate keypair for auth");
- let public_key_string =
- String::try_from(public_key).expect("failed to serialize public key for auth");
-
- if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) {
- return Self::authenticate_as_admin(http, login.clone(), token.clone()).await;
- }
-
- // Start an HTTP server to receive the redirect from Zed's sign-in page.
- let server = tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port");
- let port = server.server_addr().port();
+ let background = cx.background_executor().clone();
- // Open the Zed sign-in page in the user's browser, with query parameters that indicate
- // that the user is signing in from a Zed app running on the same device.
- let mut url = format!(
- "{}/native_app_signin?native_app_port={}&native_app_public_key={}",
- *ZED_SERVER_URL, port, public_key_string
- );
+ let (open_url_tx, open_url_rx) = oneshot::channel::<String>();
+ cx.update(|cx| {
+ cx.spawn(move |cx| async move {
+ let url = open_url_rx.await?;
+ cx.update(|cx| cx.open_url(&url))
+ })
+ .detach_and_log_err(cx);
+ })
+ .log_err();
+
+ let credentials = background
+ .clone()
+ .spawn(async move {
+ // Generate a pair of asymmetric encryption keys. The public key will be used by the
+ // zed server to encrypt the user's access token, so that it can'be intercepted by
+ // any other app running on the user's device.
+ let (public_key, private_key) =
+ rpc::auth::keypair().expect("failed to generate keypair for auth");
+ let public_key_string = String::try_from(public_key)
+ .expect("failed to serialize public key for auth");
+
+ if let Some((login, token)) =
+ IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref())
+ {
+ return Self::authenticate_as_admin(http, login.clone(), token.clone())
+ .await;
+ }
- if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() {
- log::info!("impersonating user @{}", impersonate_login);
- write!(&mut url, "&impersonate={}", impersonate_login).unwrap();
- }
+ // Start an HTTP server to receive the redirect from Zed's sign-in page.
+ let server =
+ tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port");
+ let port = server.server_addr().port();
+
+ // Open the Zed sign-in page in the user's browser, with query parameters that indicate
+ // that the user is signing in from a Zed app running on the same device.
+ let mut url = format!(
+ "{}/native_app_signin?native_app_port={}&native_app_public_key={}",
+ *ZED_SERVER_URL, port, public_key_string
+ );
+
+ if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() {
+ log::info!("impersonating user @{}", impersonate_login);
+ write!(&mut url, "&impersonate={}", impersonate_login).unwrap();
+ }
- cx.update(|cx| cx.open_url(&url))?;
-
- // Receive the HTTP request from the user's browser. Retrieve the user id and encrypted
- // access token from the query params.
- //
- // TODO - Avoid ever starting more than one HTTP server. Maybe switch to using a
- // custom URL scheme instead of this local HTTP server.
- let (user_id, access_token) = cx
- .spawn(|_| async move {
- for _ in 0..100 {
- if let Some(req) = server.recv_timeout(Duration::from_secs(1))? {
- let path = req.url();
- let mut user_id = None;
- let mut access_token = None;
- let url = Url::parse(&format!("http://example.com{}", path))
- .context("failed to parse login notification url")?;
- for (key, value) in url.query_pairs() {
- if key == "access_token" {
- access_token = Some(value.to_string());
- } else if key == "user_id" {
- user_id = Some(value.to_string());
+ open_url_tx.send(url).log_err();
+
+ // Receive the HTTP request from the user's browser. Retrieve the user id and encrypted
+ // access token from the query params.
+ //
+ // TODO - Avoid ever starting more than one HTTP server. Maybe switch to using a
+ // custom URL scheme instead of this local HTTP server.
+ let (user_id, access_token) = background
+ .spawn(async move {
+ for _ in 0..100 {
+ if let Some(req) = server.recv_timeout(Duration::from_secs(1))? {
+ let path = req.url();
+ let mut user_id = None;
+ let mut access_token = None;
+ let url = Url::parse(&format!("http://example.com{}", path))
+ .context("failed to parse login notification url")?;
+ for (key, value) in url.query_pairs() {
+ if key == "access_token" {
+ access_token = Some(value.to_string());
+ } else if key == "user_id" {
+ user_id = Some(value.to_string());
+ }
+ }
+
+ let post_auth_url =
+ format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL);
+ req.respond(
+ tiny_http::Response::empty(302).with_header(
+ tiny_http::Header::from_bytes(
+ &b"Location"[..],
+ post_auth_url.as_bytes(),
+ )
+ .unwrap(),
+ ),
+ )
+ .context("failed to respond to login http request")?;
+ return Ok((
+ user_id
+ .ok_or_else(|| anyhow!("missing user_id parameter"))?,
+ access_token.ok_or_else(|| {
+ anyhow!("missing access_token parameter")
+ })?,
+ ));
}
}
- let post_auth_url =
- format!("{}/native_app_signin_succeeded", *ZED_SERVER_URL);
- req.respond(
- tiny_http::Response::empty(302).with_header(
- tiny_http::Header::from_bytes(
- &b"Location"[..],
- post_auth_url.as_bytes(),
- )
- .unwrap(),
- ),
- )
- .context("failed to respond to login http request")?;
- return Ok((
- user_id.ok_or_else(|| anyhow!("missing user_id parameter"))?,
- access_token
- .ok_or_else(|| anyhow!("missing access_token parameter"))?,
- ));
- }
- }
+ Err(anyhow!("didn't receive login redirect"))
+ })
+ .await?;
- Err(anyhow!("didn't receive login redirect"))
+ let access_token = private_key
+ .decrypt_string(&access_token)
+ .context("failed to decrypt access token")?;
+
+ Ok(Credentials {
+ user_id: user_id.parse()?,
+ access_token,
+ })
})
.await?;
- let access_token = private_key
- .decrypt_string(&access_token)
- .context("failed to decrypt access token")?;
cx.update(|cx| cx.activate(true))?;
-
- Ok(Credentials {
- user_id: user_id.parse()?,
- access_token,
- })
+ Ok(credentials)
})
}
@@ -22,7 +22,7 @@ test-support = [
]
[dependencies]
-# auto_update = { path = "../auto_update" }
+auto_update = { package = "auto_update2", path = "../auto_update2" }
db = { package = "db2", path = "../db2" }
call = { package = "call2", path = "../call2" }
client = { package = "client2", path = "../client2" }
@@ -32,7 +32,7 @@ collections = { path = "../collections" }
# context_menu = { path = "../context_menu" }
# drag_and_drop = { path = "../drag_and_drop" }
editor = { package="editor2", path = "../editor2" }
-#feedback = { path = "../feedback" }
+feedback = { package = "feedback2", path = "../feedback2" }
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" }
@@ -46,7 +46,7 @@ rpc = { package ="rpc2", path = "../rpc2" }
settings = { package = "settings2", path = "../settings2" }
feature_flags = { package = "feature_flags2", path = "../feature_flags2"}
theme = { package = "theme2", path = "../theme2" }
-# theme_selector = { path = "../theme_selector" }
+theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
vcs_menu = { package = "vcs_menu2", path = "../vcs_menu2" }
ui = { package = "ui2", path = "../ui2" }
util = { path = "../util" }
@@ -1,9 +1,10 @@
use crate::face_pile::FacePile;
+use auto_update::AutoUpdateStatus;
use call::{ActiveCall, ParticipantLocation, Room};
use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore};
use gpui::{
- actions, canvas, div, overlay, point, px, rems, AnyElement, AppContext, DismissEvent, Div,
- Element, FocusableView, Hsla, InteractiveElement, IntoElement, Model, ParentElement, Path,
+ actions, canvas, div, overlay, point, px, rems, Action, AnyElement, AppContext, DismissEvent,
+ Div, Element, FocusableView, Hsla, InteractiveElement, IntoElement, Model, ParentElement, Path,
Render, Stateful, StatefulInteractiveElement, Styled, Subscription, View, ViewContext,
VisualContext, WeakView, WindowBounds,
};
@@ -53,7 +54,6 @@ pub struct CollabTitlebarItem {
workspace: WeakView<Workspace>,
branch_popover: Option<View<BranchList>>,
project_popover: Option<recent_projects::RecentProjects>,
- //user_menu: ViewHandle<ContextMenu>,
_subscriptions: Vec<Subscription>,
}
@@ -233,68 +233,17 @@ impl Render for CollabTitlebarItem {
}),
)
})
- .child(
- h_stack()
- .border_color(gpui::red())
- .border_1()
- .px_1p5()
- .map(|this| {
- if let Some(user) = current_user {
- // TODO: Finish implementing user menu popover
- //
- this.child(
- popover_menu("user-menu")
- .menu(|cx| {
- ContextMenu::build(cx, |menu, _| {
- menu.header("ADADA")
- })
- })
- .trigger(
- ButtonLike::new("user-menu")
- .child(
- h_stack()
- .gap_0p5()
- .child(Avatar::new(
- user.avatar_uri.clone(),
- ))
- .child(
- IconElement::new(Icon::ChevronDown)
- .color(Color::Muted),
- ),
- )
- .style(ButtonStyle::Subtle)
- .tooltip(move |cx| {
- Tooltip::text("Toggle User Menu", cx)
- }),
- )
- .anchor(gpui::AnchorCorner::TopRight),
- )
- // this.child(
- // ButtonLike::new("user-menu")
- // .child(
- // h_stack().gap_0p5().child(Avatar::data(avatar)).child(
- // IconElement::new(Icon::ChevronDown).color(Color::Muted),
- // ),
- // )
- // .style(ButtonStyle::Subtle)
- // .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
- // )
- } else {
- this.child(Button::new("sign_in", "Sign in").on_click(
- move |_, cx| {
- let client = client.clone();
- cx.spawn(move |mut cx| async move {
- client
- .authenticate_and_connect(true, &cx)
- .await
- .notify_async_err(&mut cx);
- })
- .detach();
- },
- ))
- }
- }),
- ),
+ .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))
+ .child(self.render_sign_in_button(cx))
+ .child(self.render_user_menu_button(cx))
+ }
+ }),
)
}
}
@@ -336,12 +285,6 @@ impl CollabTitlebarItem {
project,
user_store,
client,
- // user_menu: cx.add_view(|cx| {
- // let view_id = cx.view_id();
- // let mut menu = ContextMenu::new(view_id, cx);
- // menu.set_position_mode(OverlayPositionMode::Local);
- // menu
- // }),
branch_popover: None,
project_popover: None,
_subscriptions: subscriptions,
@@ -582,154 +525,113 @@ impl CollabTitlebarItem {
cx.notify();
}
- // fn render_user_menu_button(
- // &self,
- // theme: &Theme,
- // avatar: Option<Arc<ImageData>>,
- // cx: &mut ViewContext<Self>,
- // ) -> AnyElement<Self> {
- // let tooltip = theme.tooltip.clone();
- // let user_menu_button_style = if avatar.is_some() {
- // &theme.titlebar.user_menu.user_menu_button_online
- // } else {
- // &theme.titlebar.user_menu.user_menu_button_offline
- // };
-
- // let avatar_style = &user_menu_button_style.avatar;
- // Stack::new()
- // .with_child(
- // MouseEventHandler::new::<ToggleUserMenu, _>(0, cx, |state, _| {
- // let style = user_menu_button_style
- // .user_menu
- // .inactive_state()
- // .style_for(state);
-
- // let mut dropdown = Flex::row().align_children_center();
-
- // if let Some(avatar_img) = avatar {
- // dropdown = dropdown.with_child(Self::render_face(
- // avatar_img,
- // *avatar_style,
- // Color::transparent_black(),
- // None,
- // ));
- // };
-
- // dropdown
- // .with_child(
- // Svg::new("icons/caret_down.svg")
- // .with_color(user_menu_button_style.icon.color)
- // .constrained()
- // .with_width(user_menu_button_style.icon.width)
- // .contained()
- // .into_any(),
- // )
- // .aligned()
- // .constrained()
- // .with_height(style.width)
- // .contained()
- // .with_style(style.container)
- // .into_any()
- // })
- // .with_cursor_style(CursorStyle::PointingHand)
- // .on_down(MouseButton::Left, move |_, this, cx| {
- // this.user_menu.update(cx, |menu, _| menu.delay_cancel());
- // })
- // .on_click(MouseButton::Left, move |_, this, cx| {
- // this.toggle_user_menu(&Default::default(), cx)
- // })
- // .with_tooltip::<ToggleUserMenu>(
- // 0,
- // "Toggle User Menu".to_owned(),
- // Some(Box::new(ToggleUserMenu)),
- // tooltip,
- // cx,
- // )
- // .contained(),
- // )
- // .with_child(
- // ChildView::new(&self.user_menu, cx)
- // .aligned()
- // .bottom()
- // .right(),
- // )
- // .into_any()
- // }
-
- // fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- // let titlebar = &theme.titlebar;
- // MouseEventHandler::new::<SignIn, _>(0, cx, |state, _| {
- // let style = titlebar.sign_in_button.inactive_state().style_for(state);
- // Label::new("Sign In", style.text.clone())
- // .contained()
- // .with_style(style.container)
- // })
- // .with_cursor_style(CursorStyle::PointingHand)
- // .on_click(MouseButton::Left, move |_, this, cx| {
- // let client = this.client.clone();
- // cx.app_context()
- // .spawn(|cx| async move { client.authenticate_and_connect(true, &cx).await })
- // .detach_and_log_err(cx);
- // })
- // .into_any()
- // }
-
- // fn render_connection_status(
- // &self,
- // status: &client::Status,
- // cx: &mut ViewContext<Self>,
- // ) -> Option<AnyElement<Self>> {
- // enum ConnectionStatusButton {}
-
- // let theme = &theme::current(cx).clone();
- // match status {
- // client::Status::ConnectionError
- // | client::Status::ConnectionLost
- // | client::Status::Reauthenticating { .. }
- // | client::Status::Reconnecting { .. }
- // | client::Status::ReconnectionError { .. } => Some(
- // Svg::new("icons/disconnected.svg")
- // .with_color(theme.titlebar.offline_icon.color)
- // .constrained()
- // .with_width(theme.titlebar.offline_icon.width)
- // .aligned()
- // .contained()
- // .with_style(theme.titlebar.offline_icon.container)
- // .into_any(),
- // ),
- // client::Status::UpgradeRequired => {
- // let auto_updater = auto_update::AutoUpdater::get(cx);
- // let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
- // Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
- // Some(AutoUpdateStatus::Installing)
- // | Some(AutoUpdateStatus::Downloading)
- // | Some(AutoUpdateStatus::Checking) => "Updating...",
- // Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
- // "Please update Zed to Collaborate"
- // }
- // };
-
- // Some(
- // MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
- // Label::new(label, theme.titlebar.outdated_warning.text.clone())
- // .contained()
- // .with_style(theme.titlebar.outdated_warning.container)
- // .aligned()
- // })
- // .with_cursor_style(CursorStyle::PointingHand)
- // .on_click(MouseButton::Left, |_, _, cx| {
- // if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
- // if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
- // workspace::restart(&Default::default(), cx);
- // return;
- // }
- // }
- // auto_update::check(&Default::default(), cx);
- // })
- // .into_any(),
- // )
- // }
- // _ => None,
- // }
- // }
+ fn render_connection_status(
+ &self,
+ status: &client::Status,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<AnyElement> {
+ match status {
+ client::Status::ConnectionError
+ | client::Status::ConnectionLost
+ | client::Status::Reauthenticating { .. }
+ | client::Status::Reconnecting { .. }
+ | client::Status::ReconnectionError { .. } => Some(
+ div()
+ .id("disconnected")
+ .bg(gpui::red()) // todo!() @nate
+ .child(IconElement::new(Icon::Disconnected))
+ .tooltip(|cx| Tooltip::text("Disconnected", cx))
+ .into_any_element(),
+ ),
+ client::Status::UpgradeRequired => {
+ let auto_updater = auto_update::AutoUpdater::get(cx);
+ let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
+ Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
+ Some(AutoUpdateStatus::Installing)
+ | Some(AutoUpdateStatus::Downloading)
+ | Some(AutoUpdateStatus::Checking) => "Updating...",
+ Some(AutoUpdateStatus::Idle) | Some(AutoUpdateStatus::Errored) | None => {
+ "Please update Zed to Collaborate"
+ }
+ };
+
+ Some(
+ div()
+ .bg(gpui::red()) // todo!() @nate
+ .child(Button::new("connection-status", label).on_click(|_, cx| {
+ if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
+ if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
+ workspace::restart(&Default::default(), cx);
+ return;
+ }
+ }
+ auto_update::check(&Default::default(), cx);
+ }))
+ .into_any_element(),
+ )
+ }
+ _ => None,
+ }
+ }
+
+ pub fn render_sign_in_button(&mut self, _: &mut ViewContext<Self>) -> Button {
+ let client = self.client.clone();
+ Button::new("sign_in", "Sign in").on_click(move |_, cx| {
+ let client = client.clone();
+ cx.spawn(move |mut cx| async move {
+ client
+ .authenticate_and_connect(true, &cx)
+ .await
+ .notify_async_err(&mut cx);
+ })
+ .detach();
+ })
+ }
+
+ pub fn render_user_menu_button(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
+ if let Some(user) = self.user_store.read(cx).current_user() {
+ popover_menu("user-menu")
+ .menu(|cx| {
+ ContextMenu::build(cx, |menu, _| {
+ menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
+ .action("Theme", theme_selector::Toggle.boxed_clone())
+ .separator()
+ .action("Share Feedback", feedback::GiveFeedback.boxed_clone())
+ .action("Sign Out", client::SignOut.boxed_clone())
+ })
+ })
+ .trigger(
+ ButtonLike::new("user-menu")
+ .child(
+ h_stack()
+ .gap_0p5()
+ .child(Avatar::new(user.avatar_uri.clone()))
+ .child(IconElement::new(Icon::ChevronDown).color(Color::Muted)),
+ )
+ .style(ButtonStyle::Subtle)
+ .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
+ )
+ .anchor(gpui::AnchorCorner::TopRight)
+ } else {
+ popover_menu("user-menu")
+ .menu(|cx| {
+ ContextMenu::build(cx, |menu, _| {
+ menu.action("Settings", zed_actions::OpenSettings.boxed_clone())
+ .action("Theme", theme_selector::Toggle.boxed_clone())
+ .separator()
+ .action("Share Feedback", feedback::GiveFeedback.boxed_clone())
+ })
+ })
+ .trigger(
+ ButtonLike::new("user-menu")
+ .child(
+ h_stack()
+ .gap_0p5()
+ .child(IconElement::new(Icon::ChevronDown).color(Color::Muted)),
+ )
+ .style(ButtonStyle::Subtle)
+ .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
+ )
+ }
+ }
}
@@ -50,6 +50,7 @@ pub enum Icon {
CopilotError,
CopilotDisabled,
Dash,
+ Disconnected,
Envelope,
ExternalLink,
ExclamationTriangle,
@@ -129,6 +130,7 @@ impl Icon {
Icon::CopilotError => "icons/copilot_error.svg",
Icon::CopilotDisabled => "icons/copilot_disabled.svg",
Icon::Dash => "icons/dash.svg",
+ Icon::Disconnected => "icons/disconnected.svg",
Icon::Envelope => "icons/feedback.svg",
Icon::ExclamationTriangle => "icons/warning.svg",
Icon::ExternalLink => "icons/external_link.svg",
@@ -41,7 +41,7 @@ use workspace::{
notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile,
NewWindow, Workspace, WorkspaceSettings,
};
-use zed_actions::{OpenBrowser, OpenZedURL, Quit};
+use zed_actions::{OpenBrowser, OpenSettings, OpenZedURL, Quit};
actions!(
zed,
@@ -59,7 +59,6 @@ actions!(
OpenLicenses,
OpenLocalSettings,
OpenLog,
- OpenSettings,
OpenTelemetryLog,
ResetBufferFontSize,
ResetDatabase,
@@ -22,4 +22,4 @@ pub struct OpenZedURL {
impl_actions!(zed, [OpenBrowser, OpenZedURL]);
-actions!(zed, [Quit]);
+actions!(zed, [OpenSettings, Quit]);