This commit is contained in:
2026-08-31 15:11:32 +07:00
parent a2ef6bb1a3
commit 1dae0cb4b5
5 changed files with 364 additions and 48 deletions
@@ -204,21 +204,22 @@ fn list(id: &'static str, items: impl IntoIterator<Item = String>, cx: &App) ->
.min_w_0()
.children(items.into_iter().enumerate().map(|(ix, item)| {
h_flex()
.id(ix)
.h_8()
.px_2()
.gap_2()
.items_center()
.min_w_0()
.bg(cx.theme().secondary)
.hover(|this| this.bg(cx.theme().secondary_hover))
.rounded(cx.theme().radius)
.text_color(cx.theme().secondary_foreground)
.child(
h_flex()
.h_5()
.px_1()
div()
.flex_1()
.min_w_0()
.overflow_hidden()
.whitespace_nowrap()
.text_ellipsis()
.text_sm()
.bg(cx.theme().muted)
.rounded(cx.theme().radius)
.child(SharedString::from(middle_truncate(&item, 28, 16))),
)
.child(
+147 -21
View File
@@ -13,12 +13,14 @@ use gpui::{
Task, WeakEntity, Window, div, px, relative, size,
};
use gpui_base::{Button as BaseButton, Disableable, Popover};
use gpui_component::alert::Alert;
use gpui_component::avatar::Avatar;
use gpui_component::button::{Button, ButtonVariants};
use gpui_component::clipboard::Clipboard;
use gpui_component::combobox::{
Caret, Combobox, ComboboxEvent, ComboboxState, ComboboxTriggerContext,
};
use gpui_component::menu::DropdownMenu;
use gpui_component::searchable_list::SearchableVec;
use gpui_component::tree::TreeState;
use gpui_component::{
@@ -28,7 +30,7 @@ use gpui_component::{
use nostr::prelude::{RelayUrl, ToBech32};
use signed_core::Announcement;
use signed_git::{CommitList, FileCommit};
use signed_state::{GitStore, LocalReposStore, ProfileStore, RepoStore};
use signed_state::{Backend, GitStore, LocalReposStore, ProfileStore, RepoStore};
use crate::image_cache::{MAX_IMAGES, image_cache};
use crate::pixel_avatar::PixelAvatar;
@@ -74,6 +76,12 @@ enum RepoAction {
NewIssue,
/// Open the "new pull request" dialog.
NewPR,
/// Open the about dialog.
About,
/// Re-push the repository to its grasp servers.
Push,
/// Delete the repository from nostr (owner only).
Delete,
}
/// Everything loaded from the local clone for the explorer: the tree seeds,
@@ -148,6 +156,8 @@ pub struct RepoDetailView {
loading: bool,
/// The header clone button is cloning into a user-chosen folder.
cloning: bool,
/// A push to the grasp servers is in flight.
pushing: bool,
error: Option<SharedString>,
/// Commit HEAD currently points to, shown in the header button.
head_commit: Option<FileCommit>,
@@ -281,6 +291,7 @@ impl RepoDetailView {
item_sizes: Rc::new(Vec::new()),
loading: true,
cloning: false,
pushing: false,
error: None,
head_commit: None,
branch_select,
@@ -828,6 +839,60 @@ impl RepoDetailView {
});
}
/// Re-push the repository's refs to its announced grasp servers; the
/// menu trigger shows a spinner while the push is in flight, failures
/// appear in the panel's error banner.
fn push_repository(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.pushing {
return;
}
let Some(announcement) = self.announcement(cx).cloned() else {
return;
};
self.pushing = true;
self.error = None;
cx.notify();
let backend = Backend::global(cx);
let task = backend.update(cx, |backend, cx| backend.push_repository(announcement, cx));
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
let result = task.await;
this.update_in(cx, |this, _window, cx| {
if let Err(error) = result {
this.error = Some(format!("Push failed: {error}").into());
}
this.pushing = false;
cx.notify();
})?;
Ok(())
}));
}
/// Delete the repository from nostr (announcement, state and activity);
/// only offered to the repository owner. The sidebar list updates when
/// the deletion events arrive.
fn delete_repository(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(announcement) = self.announcement(cx).cloned() else {
return;
};
let backend = Backend::global(cx);
let task = backend.update(cx, |backend, cx| {
backend.delete_repository(announcement.addr(), cx)
});
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
let result = task.await;
this.update_in(cx, |this, _window, cx| {
if let Err(error) = result {
this.error = Some(format!("Delete failed: {error}").into());
}
cx.notify();
})?;
Ok(())
}));
}
/// Open the issues panel at the bottom of the dock area.
fn open_issue_detail(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(store) = self.store.clone() else {
@@ -1145,7 +1210,12 @@ impl RepoDetailView {
return div().into_any_element();
};
let store = store_entity.read(cx);
let Some(announcement) = store.announcement.as_ref().or(self.initial.as_ref()) else {
let Some(announcement) = store
.announcement
.as_ref()
.or(self.initial.as_ref())
.cloned()
else {
return div().into_any_element();
};
let issue_count = SharedString::from(store.issue_count().to_string());
@@ -1154,9 +1224,9 @@ impl RepoDetailView {
let name = self.display_name(cx);
let description = announcement.description();
let avatar = PixelAvatar::new(format!("{}:{}", announcement.owner, announcement.id));
let share = ShareTargets::from_announcement(announcement);
let share = ShareTargets::from_announcement(&announcement);
let nostr_url = nostr_clone_url(announcement, cx);
let nostr_url = nostr_clone_url(&announcement, cx);
let ngit_command = SharedString::from(format!("git clone {nostr_url}"));
let nak_command = SharedString::from(format!("nak git clone {nostr_url}"));
let git_commands = announcement.clone_urls();
@@ -1174,6 +1244,13 @@ impl RepoDetailView {
open_new_pull_request_dialog(store, window, cx);
}
}
RepoAction::About => {
if let Some(announcement) = this.announcement(cx) {
open_about_dialog(announcement.clone(), window, cx);
}
}
RepoAction::Push => this.push_repository(window, cx),
RepoAction::Delete => this.delete_repository(window, cx),
}),
)
.px_4()
@@ -1263,11 +1340,13 @@ impl RepoDetailView {
})),
)
.dropdown_menu(|menu, _, _| {
menu.menu_element_with_icon(
IconName::Plus,
Box::new(RepoAction::NewIssue),
|_, _| div().text_xs().child("New issue"),
)
menu.menu_element(Box::new(RepoAction::NewIssue), |_, _| {
h_flex()
.gap_2()
.text_sm()
.child(Icon::new(IconName::Plus))
.child("New issue")
})
}),
)
.child(
@@ -1304,11 +1383,13 @@ impl RepoDetailView {
})),
)
.dropdown_menu(|menu, _, _| {
menu.menu_element_with_icon(
IconName::Plus,
Box::new(RepoAction::NewPR),
|_, _| div().text_xs().child("New PR"),
)
menu.menu_element(Box::new(RepoAction::NewPR), |_, _| {
h_flex()
.gap_2()
.text_sm()
.child(Icon::new(IconName::Plus))
.child("New PR")
})
}),
)
.child(
@@ -1330,15 +1411,50 @@ impl RepoDetailView {
.dropdown_menu(move |menu, _, _| share.menu(menu)),
)
.child(
Button::new("info")
.icon(IconName::Info)
.tooltip("About")
Button::new("repo-menu-open")
.icon(IconName::EllipsisVertical)
.tooltip("Repository management")
.compact()
.secondary()
.on_click(cx.listener(|this, _event, window, cx| {
if let Some(announcement) = this.announcement(cx) {
open_about_dialog(announcement.clone(), window, cx);
.loading(self.pushing)
.disabled(self.pushing)
.dropdown_menu(move |menu, _, cx| {
let backend = Backend::global(cx);
let current_user = backend.read(cx).current_user();
let owner = current_user == Some(announcement.owner);
let menu = menu.menu_element(
Box::new(RepoAction::About),
|_, _| {
h_flex()
.gap_2()
.text_sm()
.child(Icon::new(IconName::Info))
.child("About")
},
);
if owner {
menu.menu_element(Box::new(RepoAction::Push), |_, _| {
h_flex()
.gap_2()
.text_sm()
.child(Icon::new(CustomIconName::Init))
.child("Republish")
})
.separator()
.menu_element(Box::new(RepoAction::Delete), |_, cx| {
h_flex()
.gap_2()
.text_sm()
.text_color(cx.theme().danger)
.child(Icon::new(IconName::Delete))
.child("Delete")
})
} else {
menu
}
})),
}),
)
.child({
let view = cx.entity();
@@ -1781,6 +1897,16 @@ impl Render for RepoDetailView {
.id("repo")
.size_full()
.child(self.render_header(cx))
.when_some(self.error.clone(), |this, error| {
this.child(
Alert::error("repo-error", error)
.banner()
.on_close(cx.listener(|this, _event, _window, cx| {
this.error = None;
cx.notify();
})),
)
})
.map(|this| match self.active_tab {
0 => this.child(
h_flex()