wip: connect to github
This commit is contained in:
@@ -1,31 +1,95 @@
|
||||
use std::time::{Duration, Instant};
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::{AppContext, FontWeight, ParentElement, Styled, div, prelude::FluentBuilder};
|
||||
use futures_lite::StreamExt;
|
||||
use gpui::{
|
||||
BorrowAppContext, InteractiveElement, ParentElement, StatefulInteractiveElement, Styled, Timer,
|
||||
div, prelude::FluentBuilder,
|
||||
};
|
||||
use rand::RngExt;
|
||||
|
||||
use crate::{
|
||||
api, app,
|
||||
component::text::text,
|
||||
query::{self, QueryStatus, read_query, use_lazy_query},
|
||||
component::{button::button, text::text},
|
||||
query::{self, QueryStatus, fetch_query, read_query, use_query},
|
||||
storage, theme,
|
||||
};
|
||||
|
||||
pub(crate) struct GithubStepView {
|
||||
last_tick: Instant,
|
||||
has_opened_link: bool,
|
||||
placeholder_code: String,
|
||||
|
||||
create_device_code_query: query::Entity<api::auth::CreateDeviceCode>,
|
||||
request_access_token_query: Option<query::Entity<api::auth::RequestAccessToken>>,
|
||||
user_query: Option<query::Entity<api::user::Fetch>>,
|
||||
}
|
||||
|
||||
pub(crate) fn new(cx: &mut gpui::Context<GithubStepView>) -> GithubStepView {
|
||||
GithubStepView {
|
||||
last_tick: Instant::now(),
|
||||
let mut view = GithubStepView {
|
||||
has_opened_link: false,
|
||||
placeholder_code: "ABCDEFGH".to_owned(),
|
||||
create_device_code_query: use_lazy_query(api::auth::CreateDeviceCode, cx),
|
||||
}
|
||||
|
||||
create_device_code_query: use_query(api::auth::CreateDeviceCode, cx),
|
||||
request_access_token_query: None,
|
||||
user_query: None,
|
||||
};
|
||||
view.on_create(cx);
|
||||
view
|
||||
}
|
||||
|
||||
impl GithubStepView {
|
||||
const CHAR_POOL: &'static str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
|
||||
fn on_create(&mut self, cx: &mut gpui::Context<Self>) {
|
||||
cx.observe(&self.create_device_code_query, |this, _, cx| {
|
||||
let code = {
|
||||
let data = read_query(&this.create_device_code_query, cx);
|
||||
if let QueryStatus::Loaded(data) = data {
|
||||
Some(data.device_code.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(ref code) = code
|
||||
&& !this.has_opened_link
|
||||
{
|
||||
this.has_opened_link = true;
|
||||
this.begin_auth_flow(code, cx);
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.spawn(async |this, cx| {
|
||||
let mut timer = Timer::interval(Duration::from_millis(50));
|
||||
loop {
|
||||
let is_code_loaded = this.read_with(cx, |this, cx| {
|
||||
matches!(
|
||||
read_query(&this.create_device_code_query, cx),
|
||||
QueryStatus::Loaded(_)
|
||||
)
|
||||
});
|
||||
|
||||
if matches!(is_code_loaded, Ok(true) | Err(_)) {
|
||||
timer.clear();
|
||||
} else {
|
||||
this.update(cx, |this, cx| {
|
||||
this.placeholder_code = this.generate_random_code(cx);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
if let None = timer.next().await {
|
||||
break;
|
||||
};
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn open_github_auth_page(cx: &mut gpui::Context<Self>) {
|
||||
cx.open_url(api::auth::DEVICE_LOGIN_FLOW_URL);
|
||||
}
|
||||
|
||||
fn generate_random_code(&mut self, cx: &mut gpui::Context<Self>) -> String {
|
||||
let rng = app::rng(cx);
|
||||
(0..8)
|
||||
@@ -35,75 +99,195 @@ impl GithubStepView {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn copy_device_code(&self, cx: &gpui::App) {
|
||||
let query = read_query(&self.create_device_code_query, cx);
|
||||
match query {
|
||||
QueryStatus::Loaded(data) => {
|
||||
cx.write_to_clipboard(gpui::ClipboardItem::new_string(data.user_code.clone()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn begin_auth_flow(&mut self, device_code: &str, cx: &mut gpui::Context<Self>) {
|
||||
GithubStepView::open_github_auth_page(cx);
|
||||
|
||||
let query = use_query(
|
||||
api::auth::RequestAccessToken {
|
||||
device_code: device_code.to_owned(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.observe(&query, |this, _, cx| {
|
||||
this.handle_access_token_query_response(cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
self.has_opened_link = true;
|
||||
self.request_access_token_query = Some(query);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn handle_access_token_query_response(&mut self, cx: &mut gpui::Context<Self>) {
|
||||
let Some(query) = &self.request_access_token_query else {
|
||||
return;
|
||||
};
|
||||
|
||||
match read_query(query, cx) {
|
||||
QueryStatus::Loaded(data) => {
|
||||
let auth_tokens = api::AuthTokens {
|
||||
access_token: data.access_token.clone(),
|
||||
};
|
||||
|
||||
cx.update_global::<query::Store<api::QueryContext>, _>(|store, _| {
|
||||
store.update_query_context(|c| {
|
||||
c.auth = Some(auth_tokens.clone());
|
||||
});
|
||||
});
|
||||
|
||||
cx.spawn(async move |weak, cx| {
|
||||
let ent = fetch_query(api::user::Fetch, cx).await;
|
||||
|
||||
let fut = weak
|
||||
.update(cx, move |_this, cx| {
|
||||
let Ok(query) = ent else {
|
||||
return None;
|
||||
};
|
||||
let QueryStatus::Loaded(user) = read_query(&query, cx) else {
|
||||
return None;
|
||||
};
|
||||
Some(storage::store_auth_tokens(&auth_tokens, user, cx))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let r = if let Some(task) = fut {
|
||||
task.await
|
||||
} else {
|
||||
Err(anyhow::Error::msg(""))
|
||||
};
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
QueryStatus::Err(api::Error::Github(api::GithubError { error, .. })) => {
|
||||
if error == "authorization_pending" {
|
||||
cx.spawn(async |weak, cx| {
|
||||
Timer::after(Duration::from_secs(1)).await;
|
||||
if let Ok(Some(query)) =
|
||||
weak.read_with(cx, |this, _cx| this.request_access_token_query.clone())
|
||||
{
|
||||
weak.update(cx, |_this, cx| {
|
||||
query.refetch(cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::Render for GithubStepView {
|
||||
fn render(
|
||||
&mut self,
|
||||
window: &mut gpui::Window,
|
||||
_window: &mut gpui::Window,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> impl gpui::IntoElement {
|
||||
let theme = app::current_theme(cx);
|
||||
|
||||
let border_color = theme.colors.surface_elevated.clone();
|
||||
let bg_color = theme.colors.surface.clone();
|
||||
|
||||
let create_device_code_query = read_query(&self.create_device_code_query, cx);
|
||||
let is_loading_code = matches!(create_device_code_query, QueryStatus::Loading);
|
||||
|
||||
let now = Instant::now();
|
||||
let should_tick = now.duration_since(self.last_tick) >= Duration::from_millis(50);
|
||||
let theme = app::current_theme(cx);
|
||||
|
||||
if is_loading_code {
|
||||
cx.on_next_frame(window, move |this, _, cx| {
|
||||
if should_tick {
|
||||
this.placeholder_code = this.generate_random_code(cx);
|
||||
this.last_tick = Instant::now();
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
let letter_boxes = self
|
||||
.placeholder_code
|
||||
.split("")
|
||||
.filter(|c| !c.is_empty())
|
||||
.map(|c| {
|
||||
text(String::from(c))
|
||||
.bold()
|
||||
.text_2xl()
|
||||
.styled(move |it| {
|
||||
it.p_3()
|
||||
.font_family("CommitMono")
|
||||
.border_1()
|
||||
.border_color(border_color)
|
||||
.rounded_lg()
|
||||
.bg(bg_color)
|
||||
})
|
||||
.when(is_loading_code, |it| it.opacity(0.5))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let displayed_code = match create_device_code_query {
|
||||
QueryStatus::Loaded(data) => &data.user_code,
|
||||
_ => &self.placeholder_code,
|
||||
};
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.px_4()
|
||||
.py_12()
|
||||
.pt_12()
|
||||
.child(header())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.flex_col()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_1p5()
|
||||
.children(letter_boxes),
|
||||
.child(
|
||||
device_code_area(displayed_code, is_loading_code, theme).on_click(
|
||||
cx.listener(|this, _, _, cx| {
|
||||
this.copy_device_code(cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
text(if is_loading_code {
|
||||
"Loading..."
|
||||
} else {
|
||||
"Click to copy"
|
||||
})
|
||||
.text_sm()
|
||||
.opacity(0.5),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().flex().flex_row().justify_end().w_full().pb_4().child(
|
||||
button("connect-to-github-next")
|
||||
.label("Next")
|
||||
.when(is_loading_code, |it| it.disabled()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn device_code_area(
|
||||
code: &String,
|
||||
is_loading: bool,
|
||||
theme: &theme::Theme,
|
||||
) -> gpui::Stateful<gpui::Div> {
|
||||
let border_color = theme.colors.border.clone();
|
||||
let bg_color = theme.colors.background.clone();
|
||||
|
||||
let letter_boxes = code
|
||||
.split("")
|
||||
.filter(|c| !c.is_empty())
|
||||
.map(|c| {
|
||||
text(String::from(c))
|
||||
.bold()
|
||||
.text_2xl()
|
||||
.styled(move |it| {
|
||||
it.p_3()
|
||||
.font_family("CommitMono")
|
||||
.border_1()
|
||||
.border_color(border_color)
|
||||
.rounded_lg()
|
||||
.bg(bg_color)
|
||||
})
|
||||
.when(is_loading, |it| it.opacity(0.5))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
div()
|
||||
.id("github-device-code-area")
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_1p5()
|
||||
.children(letter_boxes)
|
||||
}
|
||||
|
||||
fn header() -> impl gpui::IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
|
||||
@@ -59,8 +59,6 @@ impl gpui::Render for Screen {
|
||||
let theme = app::current_theme(cx);
|
||||
|
||||
div()
|
||||
.id("awd")
|
||||
.on_click(cx.listener(|a, b, c, d| {}))
|
||||
.flex()
|
||||
.flex_row()
|
||||
.items_center()
|
||||
|
||||
Reference in New Issue
Block a user