update
This commit is contained in:
@@ -11,6 +11,7 @@ use signed_core::{
|
|||||||
Announcement, Deletions, RepoAddr, RepoStatus, filters, parse_state, pull_request_patch,
|
Announcement, Deletions, RepoAddr, RepoStatus, filters, parse_state, pull_request_patch,
|
||||||
pull_request_patches,
|
pull_request_patches,
|
||||||
};
|
};
|
||||||
|
use signed_git::Nip34Binding;
|
||||||
|
|
||||||
use crate::backend::{
|
use crate::backend::{
|
||||||
Backend, BackendEvent, grasp_base_url, grasp06_prs_url, pr_clone_urls, require_relay_accepted,
|
Backend, BackendEvent, grasp_base_url, grasp06_prs_url, pr_clone_urls, require_relay_accepted,
|
||||||
@@ -39,6 +40,8 @@ pub struct RepoStore {
|
|||||||
/// Local working copy. The scan path for a local repository, kept when it is
|
/// Local working copy. The scan path for a local repository, kept when it is
|
||||||
/// later announced so the panel keeps its worktree.
|
/// later announced so the panel keeps its worktree.
|
||||||
pub path: Option<PathBuf>,
|
pub path: Option<PathBuf>,
|
||||||
|
/// NIP-34 state detected on disk for a local repository, if any.
|
||||||
|
pub nip34: Option<Nip34Binding>,
|
||||||
/// The first local pass has been applied.
|
/// The first local pass has been applied.
|
||||||
///
|
///
|
||||||
/// Views distinguish "no data yet" from a genuinely empty repository with it.
|
/// Views distinguish "no data yet" from a genuinely empty repository with it.
|
||||||
@@ -112,6 +115,7 @@ impl RepoStore {
|
|||||||
addr: Some(addr),
|
addr: Some(addr),
|
||||||
announcement: hint,
|
announcement: hint,
|
||||||
path: None,
|
path: None,
|
||||||
|
nip34: None,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
head: None,
|
head: None,
|
||||||
issues: Vec::new(),
|
issues: Vec::new(),
|
||||||
@@ -134,11 +138,12 @@ impl RepoStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Local repository discovered by the scan, not announced to NIP-34 yet.
|
/// Local repository discovered by the scan, not announced to NIP-34 yet.
|
||||||
pub fn new_local(path: PathBuf) -> Self {
|
pub fn new_local(path: PathBuf, nip34: Option<Nip34Binding>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
addr: None,
|
addr: None,
|
||||||
announcement: None,
|
announcement: None,
|
||||||
path: Some(path),
|
path: Some(path),
|
||||||
|
nip34,
|
||||||
loaded: true,
|
loaded: true,
|
||||||
head: None,
|
head: None,
|
||||||
issues: Vec::new(),
|
issues: Vec::new(),
|
||||||
@@ -160,6 +165,18 @@ impl RepoStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An announced repository whose working copy is already on disk.
|
||||||
|
pub fn from_worktree(
|
||||||
|
addr: RepoAddr,
|
||||||
|
announcement: Announcement,
|
||||||
|
path: PathBuf,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let mut store = Self::new(addr, Some(announcement), cx);
|
||||||
|
store.path = Some(path);
|
||||||
|
store
|
||||||
|
}
|
||||||
|
|
||||||
/// Switch a local repository to its NIP-34 mode, keeping its path.
|
/// Switch a local repository to its NIP-34 mode, keeping its path.
|
||||||
pub fn announce(&mut self, announcement: Announcement, cx: &mut Context<Self>) {
|
pub fn announce(&mut self, announcement: Announcement, cx: &mut Context<Self>) {
|
||||||
self.addr = Some(announcement.addr());
|
self.addr = Some(announcement.addr());
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ use nostr::prelude::{RelayUrl, ToBech32, Url};
|
|||||||
use signed_core::{Announcement, RepoAddr, RepoStatus};
|
use signed_core::{Announcement, RepoAddr, RepoStatus};
|
||||||
use signed_git::FileCommit;
|
use signed_git::FileCommit;
|
||||||
use signed_state::{
|
use signed_state::{
|
||||||
Backend, CheckoutStatus, CheckoutsStore, LocalReposStore, ProfileStore, RepoListStore,
|
Backend, CheckoutStatus, CheckoutsStore, LocalReposStore, Nip34Binding, Nip34Kind,
|
||||||
RepoStore, ensure_repo_mirror, open_repo_mirror, pr_proposes_checkout,
|
ProfileStore, RepoListStore, RepoStore, ensure_repo_mirror, open_repo_mirror,
|
||||||
|
pr_proposes_checkout,
|
||||||
};
|
};
|
||||||
use signed_ui::{
|
use signed_ui::{
|
||||||
CountBadge, DropdownButton, PixelAvatar, UserAvatar, copy_row, menu_copy_row, middle_truncate,
|
CountBadge, DropdownButton, PixelAvatar, UserAvatar, copy_row, menu_copy_row, middle_truncate,
|
||||||
@@ -123,10 +124,26 @@ impl RepoDetailView {
|
|||||||
pub fn new_local(
|
pub fn new_local(
|
||||||
dock_area: WeakEntity<DockArea>,
|
dock_area: WeakEntity<DockArea>,
|
||||||
local_path: PathBuf,
|
local_path: PathBuf,
|
||||||
|
nip34: Option<Nip34Binding>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let store = cx.new(move |_cx| RepoStore::new_local(local_path));
|
let store = cx.new(move |_cx| RepoStore::new_local(local_path, nip34));
|
||||||
|
Self::new_common(dock_area, store, window, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A local repository whose detected binding matches an announcement.
|
||||||
|
///
|
||||||
|
/// Opens as the announced repository with the local worktree attached.
|
||||||
|
pub fn new_local_announced(
|
||||||
|
dock_area: WeakEntity<DockArea>,
|
||||||
|
announcement: Announcement,
|
||||||
|
local_path: PathBuf,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let addr = announcement.addr();
|
||||||
|
let store = cx.new(move |cx| RepoStore::from_worktree(addr, announcement, local_path, cx));
|
||||||
Self::new_common(dock_area, store, window, cx)
|
Self::new_common(dock_area, store, window, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,24 +311,17 @@ impl RepoDetailView {
|
|||||||
self.error = None;
|
self.error = None;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
let (addr, announcement, local_path) = {
|
let (announcement, local_path) = {
|
||||||
let store = self.store.read(cx);
|
let store = self.store.read(cx);
|
||||||
(
|
(store.announcement.clone(), store.path.clone())
|
||||||
store.addr().cloned(),
|
|
||||||
store.announcement.clone(),
|
|
||||||
store.path.clone(),
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Local repositories live on disk at their scan path.
|
// A repository with a local worktree shows it directly. An announced one
|
||||||
// No clone step or network refresh applies here.
|
// still loads its announcement and activity from the store, which is
|
||||||
if addr.is_none() {
|
// subscribed to the relays independently.
|
||||||
|
if let Some(local_path) = local_path {
|
||||||
self.repo_started = true;
|
self.repo_started = true;
|
||||||
|
|
||||||
let Some(local_path) = local_path else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let task: gpui::Task<Result<(), Error>> = cx.spawn_in(window, async move |this, cx| {
|
let task: gpui::Task<Result<(), Error>> = cx.spawn_in(window, async move |this, cx| {
|
||||||
let data = cx
|
let data = cx
|
||||||
.background_spawn(async move {
|
.background_spawn(async move {
|
||||||
@@ -1254,6 +1264,24 @@ impl RepoDetailView {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let avatar = PixelAvatar::new(path.clone());
|
let avatar = PixelAvatar::new(path.clone());
|
||||||
|
|
||||||
|
// A repository already bound to a coordinate is not offered for publishing again.
|
||||||
|
let bound = self.store.read(cx).nip34.clone();
|
||||||
|
let action = match bound
|
||||||
|
.as_ref()
|
||||||
|
.filter(|binding| binding.kind == Nip34Kind::Initialized)
|
||||||
|
{
|
||||||
|
Some(binding) => bound_repo_label(binding, cx),
|
||||||
|
None => Button::new("init")
|
||||||
|
.icon(CustomIconName::Init)
|
||||||
|
.label("Initialize on Nostr")
|
||||||
|
.primary()
|
||||||
|
.tooltip("Publish this repository to Nostr")
|
||||||
|
.on_click(cx.listener(|this, _event, window, cx| {
|
||||||
|
this.open_init_dialog(window, cx);
|
||||||
|
}))
|
||||||
|
.into_any_element(),
|
||||||
|
};
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.px_4()
|
.px_4()
|
||||||
.pb_4()
|
.pb_4()
|
||||||
@@ -1291,16 +1319,7 @@ impl RepoDetailView {
|
|||||||
.child(path),
|
.child(path),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(action),
|
||||||
Button::new("init")
|
|
||||||
.icon(CustomIconName::Init)
|
|
||||||
.label("Initialize on Nostr")
|
|
||||||
.primary()
|
|
||||||
.tooltip("Publish this repository to Nostr")
|
|
||||||
.on_click(cx.listener(|this, _event, window, cx| {
|
|
||||||
this.open_init_dialog(window, cx);
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(self.render_header_tabs(cx))
|
.child(self.render_header_tabs(cx))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
@@ -1996,6 +2015,34 @@ pub(super) fn repo_display_name(store: &RepoStore) -> SharedString {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The owner and identifier a local repository is already bound to.
|
||||||
|
fn bound_repo_label(binding: &Nip34Binding, cx: &App) -> AnyElement {
|
||||||
|
let owner = binding
|
||||||
|
.owner
|
||||||
|
.and_then(|owner| owner.to_bech32().ok())
|
||||||
|
.map(|npub| middle_truncate(&npub, 12, 8))
|
||||||
|
.unwrap_or_else(|| "a NIP-34 coordinate".to_owned());
|
||||||
|
|
||||||
|
let mut label = v_flex().flex_shrink_0().items_end().gap_1().child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.child(SharedString::from(format!("Bound to {owner}"))),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(identifier) = binding.identifier.as_deref() {
|
||||||
|
label = label.child(
|
||||||
|
div()
|
||||||
|
.text_xs()
|
||||||
|
.font_semibold()
|
||||||
|
.text_color(cx.theme().muted_foreground)
|
||||||
|
.child(SharedString::from(identifier.to_owned())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
label.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
impl BasePanel for RepoDetailView {
|
impl BasePanel for RepoDetailView {
|
||||||
fn panel_name(&self) -> &'static str {
|
fn panel_name(&self) -> &'static str {
|
||||||
"repo"
|
"repo"
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ use dock::{
|
|||||||
};
|
};
|
||||||
use gpui::prelude::*;
|
use gpui::prelude::*;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, Context, Div, EventEmitter, FocusHandle, Focusable, Hsla, ObjectFit, Render,
|
AnyElement, App, Context, Div, Entity, EventEmitter, FocusHandle, Focusable, Hsla, ObjectFit,
|
||||||
SharedString, Subscription, WeakEntity, Window, div, img, px, relative, uniform_list, white,
|
Render, SharedString, Subscription, WeakEntity, Window, div, img, px, relative, uniform_list,
|
||||||
|
white,
|
||||||
};
|
};
|
||||||
use gpui_base::Button as BaseButton;
|
use gpui_base::Button as BaseButton;
|
||||||
use gpui_component::button::{Button, ButtonVariants};
|
use gpui_component::button::{Button, ButtonVariants};
|
||||||
@@ -264,31 +265,71 @@ impl SidebarPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The detail view offers to publish it to NIP-34.
|
/// The detail view offers to publish it to NIP-34.
|
||||||
fn open_local_repo(&mut self, path: PathBuf, window: &mut Window, cx: &mut Context<Self>) {
|
fn open_local_repo(
|
||||||
|
&mut self,
|
||||||
|
path: PathBuf,
|
||||||
|
nip34: Option<Nip34Binding>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
let detail =
|
let detail =
|
||||||
cx.new(|cx| RepoDetailView::new_local(self.dock_area.clone(), path, window, cx));
|
cx.new(|cx| RepoDetailView::new_local(self.dock_area.clone(), path, nip34, window, cx));
|
||||||
|
self.add_detail_panel(detail, window, cx);
|
||||||
self.dock_area
|
|
||||||
.update(cx, |dock_area, cx| {
|
|
||||||
add_center_panel(dock_area, panel_handle(detail), window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A local repository opens as the announced repository when its binding matches one,
|
/// A local repository whose binding matches an announcement opens as the announced repository.
|
||||||
/// and as a local-only repository otherwise.
|
fn open_local_announced(
|
||||||
|
&mut self,
|
||||||
|
announcement: Announcement,
|
||||||
|
path: PathBuf,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let detail = cx.new(|cx| {
|
||||||
|
RepoDetailView::new_local_announced(
|
||||||
|
self.dock_area.clone(),
|
||||||
|
announcement,
|
||||||
|
path,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
self.add_detail_panel(detail, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A local repository opens as the announced repository
|
||||||
|
/// when its binding matches one, and as a local-only repository otherwise.
|
||||||
fn open_local_entry(
|
fn open_local_entry(
|
||||||
&mut self,
|
&mut self,
|
||||||
entry: ResolvedLocalRepo,
|
entry: ResolvedLocalRepo,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
if let Some(announcement) = entry.announcement {
|
let ResolvedLocalRepo {
|
||||||
self.open_repo(&announcement, window, cx);
|
path,
|
||||||
|
nip34,
|
||||||
|
announcement,
|
||||||
|
} = entry;
|
||||||
|
|
||||||
|
if let Some(announcement) = announcement {
|
||||||
|
self.open_local_announced(announcement, path, window, cx);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.open_local_repo(entry.path, window, cx);
|
self.open_local_repo(path, nip34, window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_detail_panel(
|
||||||
|
&mut self,
|
||||||
|
detail: Entity<RepoDetailView>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.dock_area
|
||||||
|
.update(cx, |dock_area, cx| {
|
||||||
|
add_center_panel(dock_area, panel_handle(detail), window, cx);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_repos(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render_repos(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Local repository NIP-34 detection
|
# Local repository NIP-34 detection
|
||||||
|
|
||||||
Status: Phases 1–3 implemented. Phases 4–5 pending.
|
Status: Phases 1–4 implemented. Phase 5 optional and pending.
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
||||||
@@ -235,25 +235,27 @@ stays a thin renderer.
|
|||||||
- `ToolingOnly` → "Nostr tooling" (muted)
|
- `ToolingOnly` → "Nostr tooling" (muted)
|
||||||
- plain → unchanged warning icon.
|
- plain → unchanged warning icon.
|
||||||
|
|
||||||
Note: the announced-open path currently opens the announced repository without the local worktree;
|
A matched entry opens the announced repository; Phase 4 attaches its local worktree.
|
||||||
attaching it is Phase 4.
|
|
||||||
|
|
||||||
### Phase 4 — detail view: open as announced, gate the publish CTA
|
### Phase 4 — detail view: open as announced, gate the publish CTA
|
||||||
|
|
||||||
- [ ] `crates/signed_state/src/repo.rs`: add a constructor that keeps the worktree path while in
|
Implemented.
|
||||||
NIP-34 mode, e.g. `RepoStore::from_worktree(addr, announcement, path, cx)`, or a small
|
|
||||||
`set_path`. `RepoStore::announce` already keeps an existing path, so this can be built from
|
- [x] `crates/signed_state/src/repo.rs`: `RepoStore::from_worktree(addr, announcement, path, cx)`
|
||||||
`new` plus assigning the path. Opening a repository must **not** remove it from
|
builds the announced store and attaches the local path. `RepoStore` gained a `nip34` field
|
||||||
`LocalReposStore` (that removal belongs to `apply_announcement`, which only runs on a real
|
carrying the detected binding, and `new_local` now takes it.
|
||||||
publish).
|
- [x] `crates/workspace/src/views/repo/mod.rs`: `RepoDetailView::new_local_announced(...)` builds the
|
||||||
- [ ] `crates/workspace/src/views/repo/mod.rs`: add `RepoDetailView::new_local_announced(...)`
|
announced store with the local worktree, then `new_common`. `new_local` gained an
|
||||||
(or an `Option<Announcement>` parameter on `new_local`) that builds the announced store with
|
`Option<Nip34Binding>` parameter.
|
||||||
the local worktree attached, then `new_common`.
|
- [x] `crates/workspace/src/views/repo/mod.rs` (`load_repo`): a store with a local path now loads the
|
||||||
- [ ] `crates/workspace/src/views/sidebar/mod.rs`: route local-entry clicks through the new
|
worktree from that path even when it is announced, instead of always using the cached mirror.
|
||||||
constructor when an announcement matched, and through `open_local_repo` otherwise.
|
This is what actually attaches the local worktree; previously the announced branch ignored
|
||||||
- [ ] `crates/workspace/src/views/repo/mod.rs`: for a `new_local` view whose store carries an
|
`RepoStore::path` and cloned into the mirror.
|
||||||
`Initialized` binding, suppress the "publish to NIP-34" call to action and instead show the
|
- [x] `crates/workspace/src/views/sidebar/mod.rs`: local-entry clicks route through
|
||||||
owner and identifier. `Cloned` keeps the ability to be adopted/published.
|
`open_local_announced` when an announcement matched, and `open_local_repo` otherwise.
|
||||||
|
- [x] `crates/workspace/src/views/repo/mod.rs` (`render_local_header`): an `Initialized` binding
|
||||||
|
suppresses the "Initialize on Nostr" call to action and shows the owner and identifier
|
||||||
|
instead. `Cloned` and `ToolingOnly` keep the publish path.
|
||||||
|
|
||||||
### Phase 5 — optional: mark Signed's own publications
|
### Phase 5 — optional: mark Signed's own publications
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user