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
/// the signer flow and persisted in the keyring.
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,
}
}
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<Self>) -> 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(),
)
})
}
}
+73 -16
View File
@@ -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<DockArea>,
explore: Option<WeakEntity<RepoListView>>,
logged_in: bool,
_subscription: Subscription,
}
impl SidebarPanel {
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 {
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<Self>) {
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<Self>) -> impl IntoElement {
"Navigation"
div()
}
fn closable(&self, _cx: &App) -> bool {
false
}
fn inner_padding(&self, _cx: &App) -> bool {
false
}
}
impl EventEmitter<PanelEvent> for SidebarPanel {}
@@ -67,11 +98,37 @@ impl Focusable for SidebarPanel {
impl Render for SidebarPanel {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> 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(),
)
}
}
}
+14 -16
View File
@@ -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)