diff --git a/crates/signed_state/src/backend.rs b/crates/signed_state/src/backend.rs index fa3e145..4b2c6d8 100644 --- a/crates/signed_state/src/backend.rs +++ b/crates/signed_state/src/backend.rs @@ -218,6 +218,32 @@ impl Backend { })); } + /// Login with an `nsec1...` key or a `bunker://...` URI, dispatching on + /// the credential's prefix. + pub fn login(&mut self, credential: &str, cx: &mut Context) { + let credential = credential.trim(); + + if credential.starts_with("nsec1") { + self.login_with_nsec(credential, cx); + } else if credential.starts_with("bunker://") { + self.login_with_bunker(credential, cx); + } else { + cx.emit(BackendEvent::error( + "Unsupported credential, expected nsec1... or bunker://...", + )); + } + } + + /// Create a fresh identity and login with it. The generated key is + /// persisted in the keyring like any other `nsec` credential. + pub fn login_with_new_identity(&mut self, cx: &mut Context) { + let nsec = Keys::generate() + .secret_key() + .to_bech32() + .expect("infallible"); + self.login_with_nsec(&nsec, cx); + } + /// Login with an `nsec1...` secret key. The credential is verified by /// the signer flow and persisted in the keyring. pub fn login_with_nsec(&mut self, nsec: &str, cx: &mut Context) { diff --git a/crates/workspace/src/views/repo_list.rs b/crates/workspace/src/views/repo_list.rs index 1d6b6af..1e3f05e 100644 --- a/crates/workspace/src/views/repo_list.rs +++ b/crates/workspace/src/views/repo_list.rs @@ -26,6 +26,59 @@ impl RepoListView { _subscription: subscription, } } + + fn render_card(&self, announcement: &Announcement, cx: &mut App) -> AnyElement { + let name = announcement + .name + .clone() + .unwrap_or_else(|| announcement.id.clone()); + + let owner = ProfileStore::global(cx) + .update(cx, |store, cx| store.get(announcement.owner, cx)) + .name(); + + let description = announcement.description.clone().unwrap_or_default(); + + div() + .v_flex() + .h(px(60.)) + .w_full() + .justify_center() + .gap_1() + .px_4() + .border_b(px(1.)) + .border_color(cx.theme().border) + .child( + div() + .h_flex() + .gap_2() + .items_center() + .child( + div() + .text_sm() + .font_semibold() + .whitespace_nowrap() + .text_ellipsis() + .child(name), + ) + .child( + div() + .text_xs() + .text_color(cx.theme().muted_foreground) + .whitespace_nowrap() + .child(owner), + ), + ) + .child( + div() + .text_xs() + .text_color(cx.theme().muted_foreground) + .whitespace_nowrap() + .text_ellipsis() + .child(description), + ) + .into_any_element() + } } impl Panel for RepoListView { @@ -46,86 +99,12 @@ impl Focusable for RepoListView { } } -fn render_card(announcement: &Announcement, cx: &mut App) -> AnyElement { - let name = announcement - .name - .clone() - .unwrap_or_else(|| announcement.id.clone()); - - let owner = ProfileStore::global(cx) - .update(cx, |store, cx| store.get(announcement.owner, cx)) - .name(); - - let description = announcement.description.clone().unwrap_or_default(); - - div() - .v_flex() - .h(px(60.)) - .justify_center() - .gap_1() - .px_4() - .border_b(px(1.)) - .border_color(cx.theme().border) - .child( - div() - .h_flex() - .gap_2() - .items_center() - .child( - div() - .text_sm() - .font_semibold() - .whitespace_nowrap() - .text_ellipsis() - .child(name), - ) - .child( - div() - .text_xs() - .text_color(cx.theme().muted_foreground) - .whitespace_nowrap() - .child(owner), - ), - ) - .child( - div() - .text_xs() - .text_color(cx.theme().muted_foreground) - .whitespace_nowrap() - .text_ellipsis() - .child(description), - ) - .into_any_element() -} - impl Render for RepoListView { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let announcements = self.store.read(cx).announcements.clone(); + let has_announcements = !announcements.is_empty(); let count = announcements.len(); - let body = if announcements.is_empty() { - div() - .size_full() - .v_flex() - .items_center() - .justify_center() - .child( - div() - .text_sm() - .text_color(cx.theme().muted_foreground) - .child("No repositories found. Waiting for relays..."), - ) - .into_any_element() - } else { - uniform_list("repo-list", count, move |range, _window, cx| { - range - .map(|index| render_card(&announcements[index], cx)) - .collect() - }) - .size_full() - .into_any_element() - }; - div() .v_flex() .size_full() @@ -143,6 +122,38 @@ impl Render for RepoListView { .child(SharedString::from(format!(" ({count})"))), ), ) - .child(body) + .when(!has_announcements, |this| { + this.child( + div() + .size_full() + .v_flex() + .items_center() + .justify_center() + .child( + div() + .text_sm() + .text_color(cx.theme().muted_foreground) + .child("No repositories found. Waiting for relays..."), + ), + ) + }) + .when(has_announcements, |this| { + this.child( + uniform_list( + "repos", + count, + cx.processor(move |this, range, _window, cx| { + let mut items = vec![]; + + for ix in range { + items.push(this.render_card(&announcements[ix], cx)); + } + + items + }), + ) + .size_full(), + ) + }) } } diff --git a/crates/workspace/src/views/sidebar.rs b/crates/workspace/src/views/sidebar.rs index 5c5a62a..235dd8f 100644 --- a/crates/workspace/src/views/sidebar.rs +++ b/crates/workspace/src/views/sidebar.rs @@ -1,10 +1,14 @@ use std::sync::Arc; use gpui::prelude::*; -use gpui::{App, Context, EventEmitter, FocusHandle, Focusable, Render, WeakEntity, Window}; -use gpui_component::button::Button; +use gpui::{ + App, Context, EventEmitter, FocusHandle, Focusable, Render, Subscription, WeakEntity, Window, + div, +}; +use gpui_component::button::{Button, ButtonVariants}; use gpui_component::dock::{DockArea, DockPlacement, Panel, PanelEvent}; -use gpui_component::v_flex; +use gpui_component::{ActiveTheme, v_flex}; +use signed_state::{Backend, BackendEvent}; use crate::views::RepoListView; @@ -14,32 +18,55 @@ pub struct SidebarPanel { focus_handle: FocusHandle, dock_area: WeakEntity, explore: Option>, + logged_in: bool, + _subscription: Subscription, } impl SidebarPanel { pub fn new(dock_area: WeakEntity, cx: &mut Context) -> Self { + let backend = Backend::global(cx); + let logged_in = backend.read(cx).current_user().is_some(); + + let subscription = cx.subscribe(&backend, |this, backend, event, cx| { + match event { + BackendEvent::SignerChanged => { + this.logged_in = backend.read(cx).current_user().is_some(); + } + BackendEvent::SignerRequired => { + this.logged_in = false; + } + _ => return, + } + cx.notify(); + }); + Self { focus_handle: cx.focus_handle(), dock_area, explore: None, + logged_in, + _subscription: subscription, } } /// Open the Explore (repository list) panel in the center of the dock /// area. No-op if it's already open. pub fn open_explore(&mut self, window: &mut Window, cx: &mut Context) { - if self.explore.as_ref().and_then(WeakEntity::upgrade).is_some() { + if self + .explore + .as_ref() + .and_then(WeakEntity::upgrade) + .is_some() + { return; } let panel = cx.new(|cx| RepoListView::new(window, cx)); self.explore = Some(panel.downgrade()); - self.dock_area - .update(cx, |dock_area, cx| { - dock_area.add_panel(Arc::new(panel), DockPlacement::Center, None, window, cx); - }) - .ok(); + let _ = self.dock_area.update(cx, |dock_area, cx| { + dock_area.add_panel(Arc::new(panel), DockPlacement::Center, None, window, cx); + }); } } @@ -49,12 +76,16 @@ impl Panel for SidebarPanel { } fn title(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - "Navigation" + div() } fn closable(&self, _cx: &App) -> bool { false } + + fn inner_padding(&self, _cx: &App) -> bool { + false + } } impl EventEmitter for SidebarPanel {} @@ -67,11 +98,37 @@ impl Focusable for SidebarPanel { impl Render for SidebarPanel { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex().gap_2().p_2().child( - Button::new("explore") - .label("Explore") - .w_full() - .on_click(cx.listener(|this, _, window, cx| this.open_explore(window, cx))), - ) + if self.logged_in { + v_flex().child( + Button::new("explore") + .label("Explore") + .w_full() + .on_click(cx.listener(|this, _, window, cx| this.open_explore(window, cx))), + ) + } else { + v_flex() + .p_4() + .size_full() + .items_center() + .justify_center() + .gap_2() + .child( + div() + .text_xs() + .text_color(cx.theme().muted_foreground) + .child("Sign in to continue"), + ) + .child( + Button::new("get-started") + .label("Get started") + .primary() + .w_full(), + ) + .child( + Button::new("import-identity") + .label("Import identity") + .w_full(), + ) + } } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 333af3c..25cdcac 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1,7 +1,7 @@ use gpui::prelude::*; use gpui::{Context, Entity, Render, SharedString, Subscription, Window, div, px}; use gpui_component::dock::{DockArea, DockItem}; -use gpui_component::{ActiveTheme, Root, StyledExt, TitleBar, v_flex}; +use gpui_component::{ActiveTheme, Root, StyledExt, TitleBar, h_flex, v_flex}; use signed_state::{Backend, BackendEvent}; use crate::views::SidebarPanel; @@ -78,24 +78,22 @@ impl Render for Workspace { v_flex() .size_full() // Title Bar - .child(TitleBar::new()) - // Dock Area - .child(self.dock.clone()) - // Status Bar .child( - div() - .h_flex() - .px_4() - .py_1() - .border_t(px(1.)) - .border_color(cx.theme().border) + TitleBar::new() + // Left + .child(div()) + // Right .child( - div() - .text_xs() - .text_color(cx.theme().muted_foreground) - .child(self.status.clone()), + h_flex().px_2().child( + div() + .text_xs() + .text_color(cx.theme().muted_foreground) + .child(self.status.clone()), + ), ), - ), + ) + // Dock Area + .child(self.dock.clone()), ) // Notifications .children(notification_layer)