This commit is contained in:
2026-08-05 17:07:50 +07:00
parent 6b13d8a8f7
commit a97dfac23f
4 changed files with 200 additions and 108 deletions
+26
View File
@@ -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<Self>) {
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<Self>) {
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 /// Login with an `nsec1...` secret key. The credential is verified by
/// the signer flow and persisted in the keyring. /// the signer flow and persisted in the keyring.
pub fn login_with_nsec(&mut self, nsec: &str, cx: &mut Context<Self>) { pub fn login_with_nsec(&mut self, nsec: &str, cx: &mut Context<Self>) {
+87 -76
View File
@@ -26,6 +26,59 @@ impl RepoListView {
_subscription: subscription, _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 { 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 { impl Render for RepoListView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let announcements = self.store.read(cx).announcements.clone(); let announcements = self.store.read(cx).announcements.clone();
let has_announcements = !announcements.is_empty();
let count = announcements.len(); 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() div()
.v_flex() .v_flex()
.size_full() .size_full()
@@ -143,6 +122,38 @@ impl Render for RepoListView {
.child(SharedString::from(format!(" ({count})"))), .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(),
)
})
} }
} }
+73 -16
View File
@@ -1,10 +1,14 @@
use std::sync::Arc; use std::sync::Arc;
use gpui::prelude::*; use gpui::prelude::*;
use gpui::{App, Context, EventEmitter, FocusHandle, Focusable, Render, WeakEntity, Window}; use gpui::{
use gpui_component::button::Button; 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::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; use crate::views::RepoListView;
@@ -14,32 +18,55 @@ pub struct SidebarPanel {
focus_handle: FocusHandle, focus_handle: FocusHandle,
dock_area: WeakEntity<DockArea>, dock_area: WeakEntity<DockArea>,
explore: Option<WeakEntity<RepoListView>>, explore: Option<WeakEntity<RepoListView>>,
logged_in: bool,
_subscription: Subscription,
} }
impl SidebarPanel { impl SidebarPanel {
pub fn new(dock_area: WeakEntity<DockArea>, cx: &mut Context<Self>) -> Self { pub fn new(dock_area: WeakEntity<DockArea>, cx: &mut Context<Self>) -> 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 { Self {
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
dock_area, dock_area,
explore: None, explore: None,
logged_in,
_subscription: subscription,
} }
} }
/// Open the Explore (repository list) panel in the center of the dock /// Open the Explore (repository list) panel in the center of the dock
/// area. No-op if it's already open. /// area. No-op if it's already open.
pub fn open_explore(&mut self, window: &mut Window, cx: &mut Context<Self>) { pub fn open_explore(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.explore.as_ref().and_then(WeakEntity::upgrade).is_some() { if self
.explore
.as_ref()
.and_then(WeakEntity::upgrade)
.is_some()
{
return; return;
} }
let panel = cx.new(|cx| RepoListView::new(window, cx)); let panel = cx.new(|cx| RepoListView::new(window, cx));
self.explore = Some(panel.downgrade()); self.explore = Some(panel.downgrade());
self.dock_area let _ = self.dock_area.update(cx, |dock_area, cx| {
.update(cx, |dock_area, cx| { dock_area.add_panel(Arc::new(panel), DockPlacement::Center, None, window, cx);
dock_area.add_panel(Arc::new(panel), DockPlacement::Center, None, window, cx); });
})
.ok();
} }
} }
@@ -49,12 +76,16 @@ impl Panel for SidebarPanel {
} }
fn title(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement { fn title(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
"Navigation" div()
} }
fn closable(&self, _cx: &App) -> bool { fn closable(&self, _cx: &App) -> bool {
false false
} }
fn inner_padding(&self, _cx: &App) -> bool {
false
}
} }
impl EventEmitter<PanelEvent> for SidebarPanel {} impl EventEmitter<PanelEvent> for SidebarPanel {}
@@ -67,11 +98,37 @@ impl Focusable for SidebarPanel {
impl Render for SidebarPanel { impl Render for SidebarPanel {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex().gap_2().p_2().child( if self.logged_in {
Button::new("explore") v_flex().child(
.label("Explore") Button::new("explore")
.w_full() .label("Explore")
.on_click(cx.listener(|this, _, window, cx| this.open_explore(window, cx))), .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(),
)
}
} }
} }
+14 -16
View File
@@ -1,7 +1,7 @@
use gpui::prelude::*; use gpui::prelude::*;
use gpui::{Context, Entity, Render, SharedString, Subscription, Window, div, px}; use gpui::{Context, Entity, Render, SharedString, Subscription, Window, div, px};
use gpui_component::dock::{DockArea, DockItem}; 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 signed_state::{Backend, BackendEvent};
use crate::views::SidebarPanel; use crate::views::SidebarPanel;
@@ -78,24 +78,22 @@ impl Render for Workspace {
v_flex() v_flex()
.size_full() .size_full()
// Title Bar // Title Bar
.child(TitleBar::new())
// Dock Area
.child(self.dock.clone())
// Status Bar
.child( .child(
div() TitleBar::new()
.h_flex() // Left
.px_4() .child(div())
.py_1() // Right
.border_t(px(1.))
.border_color(cx.theme().border)
.child( .child(
div() h_flex().px_2().child(
.text_xs() div()
.text_color(cx.theme().muted_foreground) .text_xs()
.child(self.status.clone()), .text_color(cx.theme().muted_foreground)
.child(self.status.clone()),
),
), ),
), )
// Dock Area
.child(self.dock.clone()),
) )
// Notifications // Notifications
.children(notification_layer) .children(notification_layer)