Don't hang the app when signing in offline

This commit is contained in:
Conrad Irwin 2023-12-13 15:44:27 -07:00
parent 3094cb749e
commit 7899833367
6 changed files with 225 additions and 343 deletions

1
Cargo.lock generated
View file

@ -1949,6 +1949,7 @@ name = "collab_ui2"
version = "0.1.0"
dependencies = [
"anyhow",
"auto_update2",
"call2",
"channel2",
"client2",

View file

@ -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;
use serde::Deserialize;
@ -125,7 +125,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 {

View file

@ -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");
let background = cx.background_executor().clone();
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;
}
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();
// 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 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");
// 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((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();
cx.update(|cx| cx.open_url(&url))?;
// 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
);
// 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());
if let Some(impersonate_login) = IMPERSONATE_LOGIN.as_ref() {
log::info!("impersonating user @{}", impersonate_login);
write!(&mut url, "&impersonate={}", impersonate_login).unwrap();
}
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)
})
}

View file

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

View file

@ -1,10 +1,11 @@
use crate::face_pile::FacePile;
use auto_update::AutoUpdateStatus;
use call::{ActiveCall, ParticipantLocation, Room};
use client::{proto::PeerId, Client, ParticipantIndex, SignOut, User, UserStore};
use client::{proto::PeerId, Client, ParticipantIndex, User, UserStore};
use gpui::{
actions, canvas, div, overlay, point, px, rems, Action, AppContext, DismissEvent, Div, Element,
FocusableView, Hsla, InteractiveElement, IntoElement, Model, ParentElement, Path, Render,
Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext,
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, ViewContext, VisualContext,
WeakView, WindowBounds,
};
use project::{Project, RepositoryEntry};
@ -16,7 +17,7 @@ use ui::{
IconButton, IconElement, KeyBinding, Tooltip,
};
use util::ResultExt;
use workspace::{notifications::NotifyResultExt, Feedback, Workspace, WORKSPACE_DB};
use workspace::{notifications::NotifyResultExt, Workspace, WORKSPACE_DB};
const MAX_PROJECT_NAME_LENGTH: usize = 40;
const MAX_BRANCH_NAME_LENGTH: usize = 40;
@ -52,7 +53,6 @@ pub struct CollabTitlebarItem {
workspace: WeakView<Workspace>,
//branch_popover: Option<ViewHandle<BranchList>>,
project_popover: Option<recent_projects::RecentProjects>,
//user_menu: ViewHandle<ContextMenu>,
_subscriptions: Vec<Subscription>,
}
@ -232,88 +232,17 @@ impl Render for CollabTitlebarItem {
}),
)
})
.child(h_stack().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.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),
)
.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 {
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();
}))
.child(
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)
}),
),
)
el.children(self.render_connection_status(status, cx))
.child(self.render_sign_in_button(cx))
.child(self.render_user_menu_button(cx))
}
})),
}),
)
}
}
@ -355,12 +284,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,
@ -535,34 +458,6 @@ impl CollabTitlebarItem {
.log_err();
}
// pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
// self.user_menu.update(cx, |user_menu, cx| {
// let items = if let Some(_) = self.user_store.read(cx).current_user() {
// vec![
// ContextMenuItem::action("Settings", zed_actions::OpenSettings),
// ContextMenuItem::action("Theme", theme_selector::Toggle),
// ContextMenuItem::separator(),
// ContextMenuItem::action(
// "Share Feedback",
// feedback::feedback_editor::GiveFeedback,
// ),
// ContextMenuItem::action("Sign Out", SignOut),
// ]
// } else {
// vec![
// ContextMenuItem::action("Settings", zed_actions::OpenSettings),
// ContextMenuItem::action("Theme", theme_selector::Toggle),
// ContextMenuItem::separator(),
// ContextMenuItem::action(
// "Share Feedback",
// feedback::feedback_editor::GiveFeedback,
// ),
// ]
// };
// user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
// });
// }
// fn render_branches_popover_host<'a>(
// &'a self,
// _theme: &'a theme::Titlebar,
@ -696,154 +591,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
// };
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"
}
};
// 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);
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,
}
}
// let mut dropdown = Flex::row().align_children_center();
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();
})
}
// 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,
// }
// }
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)),
)
}
}
}

View file

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