refactor 2

This commit is contained in:
2026-09-13 12:20:49 +07:00
parent a4dcbbc560
commit 025fd15837
9 changed files with 124 additions and 128 deletions
+1 -11
View File
@@ -393,16 +393,6 @@ impl InboxView {
return;
};
let Some(announcement) = RepoListStore::global(cx)
.read(cx)
.announcements
.iter()
.find(|announcement| announcement.addr() == address)
.cloned()
else {
return;
};
let item = match kind {
Some(Kind::GitIssue) => RepoItem::Issue(root),
Some(Kind::GitPullRequest) => RepoItem::PullRequest(root),
@@ -410,7 +400,7 @@ impl InboxView {
_ => return,
};
open_repo_item(&self.dock_area, &announcement, item, window, cx);
open_repo_item(&self.dock_area, &address, None, item, window, cx);
}
fn render_entry(&self, ix: usize, cx: &Context<Self>) -> AnyElement {
+41 -78
View File
@@ -1,6 +1,5 @@
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use anyhow::Error;
use dock::{DockArea, add_center_panel, panel_handle};
@@ -8,8 +7,8 @@ use gpui::prelude::*;
use gpui::{App, Context, Entity, WeakEntity, Window};
use gpui_base::dock::PanelView;
use nostr::prelude::EventId;
use signed_core::{Announcement, filters};
use signed_state::{Backend, RepoListStore, RepoStore};
use signed_core::{Announcement, RepoAddr};
use signed_state::RepoStore;
use super::RepoDetailView;
use crate::views::issues::IssuesView;
@@ -114,74 +113,20 @@ impl RepoDetailView {
}
/// Open the upstream repository, the `u` tag of this fork's announcement.
/// The upstream announcement may not be in the local database yet.
/// Subscribe for it and open the panel as soon as it lands.
///
/// The announcement may not be in the local database yet. The panel opens
/// from the address and fills in when the store loads it; the store's
/// `subscribe_remote` fetches it from the bootstrap relays.
pub(super) fn open_upstream(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.pending_upstream.is_some() {
return;
}
let Some(announcement) = self.announcement(cx).cloned() else {
let Some(addr) = self
.announcement(cx)
.and_then(|announcement| announcement.upstream.as_ref())
.and_then(|upstream| upstream.addr.clone())
else {
return;
};
let Some(addr) = announcement.upstream.and_then(|upstream| upstream.addr) else {
return;
};
if let Some(found) = RepoListStore::global(cx)
.read(cx)
.announcements
.iter()
.find(|a| a.addr() == addr)
.cloned()
{
open_repo_panel(&self.dock_area, &found, window, &mut *cx);
return;
}
let backend = Backend::global(cx);
backend.update(cx, |backend, cx| {
backend.subscribe_bootstrap(vec![filters::announcement(&addr)], cx);
});
self.pending_upstream = Some(addr);
let task: gpui::Task<Result<(), Error>> = cx.spawn_in(window, async move |this, cx| {
for _ in 0..60 {
cx.background_executor()
.timer(Duration::from_millis(250))
.await;
let opened = this.update_in(cx, |this, window, cx| {
let Some(addr) = this.pending_upstream.clone() else {
return true;
};
let found = RepoListStore::global(cx)
.read(cx)
.announcements
.iter()
.find(|a| a.addr() == addr)
.cloned();
match found {
Some(found) => {
this.pending_upstream = None;
open_repo_panel(&this.dock_area, &found, window, &mut *cx);
true
}
None => false,
}
})?;
if opened {
return Ok(());
}
}
this.update(cx, |this, _cx| this.pending_upstream = None)?;
Ok(())
});
task.detach();
open_repo_panel(&self.dock_area, &addr, None, window, &mut *cx);
}
/// Open the dialog guiding the user through publishing the local repository to NIP-34.
@@ -194,15 +139,21 @@ impl RepoDetailView {
}
}
/// Open `announcement` as a repository panel in the dock's center.
/// Open `addr`'s repository as a panel in the dock's center.
///
/// `hint` is an announcement already in hand for `addr`. It seeds the store's
/// relays and lets the explorer load without waiting for the database; the
/// store loads the announcement itself when the hint is absent, so an entry
/// point with only an address works too.
pub(crate) fn open_repo_panel(
dock_area: &WeakEntity<DockArea>,
announcement: &Announcement,
addr: &RepoAddr,
hint: Option<&Announcement>,
window: &mut Window,
cx: &mut App,
) -> Entity<RepoDetailView> {
let detail =
cx.new(|cx| RepoDetailView::new(dock_area.clone(), announcement.clone(), window, cx));
let detail = cx
.new(|cx| RepoDetailView::new(dock_area.clone(), addr.clone(), hint.cloned(), window, cx));
if let Some(dock_area) = dock_area.upgrade() {
dock_area.update(cx, |dock_area, cx| {
@@ -213,9 +164,16 @@ pub(crate) fn open_repo_panel(
detail
}
/// The nostr store of `announcement`'s repository, without opening a repository panel.
fn repo_store(announcement: &Announcement, cx: &mut App) -> Entity<RepoStore> {
cx.new(|cx| RepoStore::new(announcement.addr(), announcement.relays.clone(), cx))
/// The nostr store of `addr`'s repository, without opening a repository panel.
///
/// `hint` is an announcement already in hand for `addr`. It only seeds the
/// relays to connect to right away; the store loads the announcement from the
/// local database on its first pass, so the hint is optional.
fn repo_store(addr: &RepoAddr, hint: Option<&Announcement>, cx: &mut App) -> Entity<RepoStore> {
let relays = hint
.map(|announcement| announcement.relays.clone())
.unwrap_or_default();
cx.new(|cx| RepoStore::new(addr.clone(), relays, cx))
}
/// An item of a repository to open from outside its detail panel.
@@ -226,16 +184,21 @@ pub(crate) enum RepoItem {
Patch,
}
/// Open the detail panel of `item` in `announcement`'s repository, in the dock's center.
/// Open the detail panel of `item` in `addr`'s repository, in the dock's center.
///
/// The repository store is built here, not taken from a `RepoDetailView`, so the
/// item panel is the only panel docked.
///
/// `hint` is an announcement already in hand for `addr`, e.g. the inbox row the
/// item was clicked from. The store resolves the repository from the local
/// database on its own, so an entry point with only the address works too.
///
/// A patch opens nothing: patches are only consumed inside a pull request's
/// detail panel, and have no panel of their own.
pub(crate) fn open_repo_item(
dock_area: &WeakEntity<DockArea>,
announcement: &Announcement,
addr: &RepoAddr,
hint: Option<&Announcement>,
item: RepoItem,
window: &mut Window,
cx: &mut App,
@@ -243,11 +206,11 @@ pub(crate) fn open_repo_item(
let panel: Arc<dyn PanelView> =
match item {
RepoItem::Issue(issue_id) => {
let store = repo_store(announcement, cx);
let store = repo_store(addr, hint, cx);
panel_handle(cx.new(|cx| IssueDetailView::new(store, issue_id, window, cx)))
}
RepoItem::PullRequest(pr_id) => {
let store = repo_store(announcement, cx);
let store = repo_store(addr, hint, cx);
panel_handle(cx.new(|cx| {
PullRequestDetailView::new(dock_area.clone(), store, pr_id, window, cx)
}))
@@ -190,7 +190,7 @@ fn init_repository(
window.close_dialog(cx);
if let Some(view) = view.upgrade() {
view.update(cx, |this, cx| {
this.apply_announcement(announcement, cx);
this.apply_announcement(announcement, window, cx);
});
}
})
+14 -19
View File
@@ -73,7 +73,8 @@ pub struct RepoDetailView {
dock_area: WeakEntity<DockArea>,
/// Snapshot taken at open time.
///
/// `None` for local repositories that haven't been published yet.
/// `None` for local repositories that haven't been published yet, and for a
/// repository opened by address until its store loads the announcement.
initial: Option<Announcement>,
/// Per-repository nostr store, holding announcement, issues, PRs and statuses.
///
@@ -162,37 +163,32 @@ pub struct RepoDetailView {
/// The global checkouts store's ready-to-push statuses of this repository,
/// last seen when they drove a render.
push_statuses: Vec<CheckoutStatus>,
/// Upstream repository, from this fork's `u` tag, the user asked to open.
/// Its announcement is still being fetched.
pending_upstream: Option<RepoAddr>,
}
impl RepoDetailView {
/// Open a repository announced.
/// Open a repository by address.
///
/// The store connects to the announcement's relays and loads issues, PRs and statuses.
/// `hint` is an announcement already in hand. It seeds the store's relays
/// and the explorer's clone URLs; without it the panel waits for the store
/// to load the announcement from the local database.
pub fn new(
dock_area: WeakEntity<DockArea>,
initial: Announcement,
addr: RepoAddr,
hint: Option<Announcement>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
// The announcement we opened from already carries the NIP-34 `relays` tag.
//
// The store connects to those relays immediately, no bootstrap fetch wait.
let addr = initial.addr();
let relays = initial.relays.clone();
let relays = hint
.as_ref()
.map(|announcement| announcement.relays.clone())
.unwrap_or_default();
let store = cx.new(|cx| RepoStore::new(addr, relays, cx));
let mut view = Self::new_common(
dock_area,
Some(initial),
Some(store.clone()),
None,
window,
cx,
);
view.attach_store(&store, cx);
let mut view = Self::new_common(dock_area, hint, Some(store.clone()), None, window, cx);
view.attach_store(&store, window, cx);
view
}
@@ -313,7 +309,6 @@ impl RepoDetailView {
ready_head: None,
ready_statuses: Vec::new(),
push_statuses: Vec::new(),
pending_upstream: None,
focus_handle: cx.focus_handle(),
_subscriptions: subscriptions,
}
+22 -5
View File
@@ -1,5 +1,5 @@
use gpui::prelude::*;
use gpui::{Context, Entity};
use gpui::{Context, Entity, Window};
use signed_core::Announcement;
use signed_state::{Backend, CheckoutsStore, LocalReposStore, RepoStore};
@@ -13,6 +13,7 @@ impl RepoDetailView {
pub(crate) fn apply_announcement(
&mut self,
announcement: Announcement,
window: &mut Window,
cx: &mut Context<Self>,
) {
// The repository is no longer a bare local repo.
@@ -24,7 +25,7 @@ impl RepoDetailView {
cx.new(|cx| RepoStore::new(announcement.addr(), announcement.relays.clone(), cx));
// Re-render on store refreshes, issues, PRs and statuses.
// Keep the ready-to-contribute statuses of this repository requested.
self.attach_store(&store, cx);
self.attach_store(&store, window, cx);
self.store = Some(store);
self.initial = Some(announcement);
cx.notify();
@@ -32,11 +33,27 @@ impl RepoDetailView {
/// Observe the repository's store, re-render on refreshes.
/// Request the ready-to-contribute statuses for it.
pub(super) fn attach_store(&mut self, store: &Entity<RepoStore>, cx: &mut Context<Self>) {
pub(super) fn attach_store(
&mut self,
store: &Entity<RepoStore>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self._subscriptions
.push(cx.observe(store, |this, _store, cx| {
log::debug!("repo detail: store notify");
.push(cx.observe_in(store, window, |this, store, window, cx| {
this.refresh_ready_statuses(cx);
// A repository opened from its address alone starts without an
// announcement. Adopt the store's first one so the explorer can
// load; later passes leave the snapshot and the selection alone.
let announcement = store.read(cx).announcement.clone();
if this.initial.is_none()
&& let Some(announcement) = announcement
{
this.initial = Some(announcement);
this.load_repo(window, cx);
}
cx.notify();
}));
self.refresh_ready_statuses(cx);
+7 -1
View File
@@ -189,7 +189,13 @@ impl RepoListView {
window: &mut Window,
cx: &mut Context<Self>,
) {
open_repo_panel(&self.dock_area, announcement, window, cx);
open_repo_panel(
&self.dock_area,
&announcement.addr(),
Some(announcement),
window,
cx,
);
}
fn render_card(
@@ -257,5 +257,11 @@ fn open_repo(
window: &mut Window,
cx: &mut App,
) {
open_repo_panel(&dock_area, &announcement, window, cx);
open_repo_panel(
&dock_area,
&announcement.addr(),
Some(&announcement),
window,
cx,
);
}
+7 -1
View File
@@ -260,7 +260,13 @@ impl SidebarPanel {
window: &mut Window,
cx: &mut Context<Self>,
) {
open_repo_panel(&self.dock_area, announcement, window, &mut *cx);
open_repo_panel(
&self.dock_area,
&announcement.addr(),
Some(announcement),
window,
&mut *cx,
);
}
/// Open a local repository's detail view in the dock's center.
+24 -11
View File
@@ -1,6 +1,6 @@
# Repository state and panel flow plan
Status: proposed (2026-09-13)
Status: phases 1-2 implemented, phase 3 next (2026-09-13)
Builds on `docs/backend-rearchitecture.md`, especially §7 (notify audit),
§11 (split independently-observed state), §12 (one debounce at the source)
@@ -237,10 +237,8 @@ flow for announced repositories.
- `open_repo_item(dock_area, addr: &RepoAddr, item, window, cx)`.
- Inbox passes its already-parsed `address` (`signed_core::InboxItem.address`,
the root event's `a` tag) and drops the `RepoListStore` lookup.
- `open_upstream` drops the 60 x 250 ms wait loop: the store's
`repo_filters` already include the announcement filter and
`subscribe_remote` runs on creation, so the panel opens immediately and
fills in.
- `open_upstream` no longer polls the list; it opens the panel by address and
the store fills it in (Phase 2).
- `RepoItem::Patch` behavior is unchanged.
## Phases
@@ -271,17 +269,32 @@ Status: implemented, except step 6.
### Phase 2 - entry points by identity
1. `views/repo/actions.rs`: `repo_store(addr, hint, cx)`;
`open_repo_item(addr, ...)`; `open_upstream` opens directly.
2. `views/inbox.rs`: pass `address`, delete the announcement lookup and its
silent early-return.
Status: implemented.
1. `views/repo/actions.rs`: `repo_store(addr, hint, cx)` seeds the store's
relays from an optional hint but needs nothing else; the store loads the
announcement itself. `open_repo_item(addr, hint, item, window, cx)` takes
the address, not a hydrated announcement.
2. `views/inbox.rs`: `open` passes its already-parsed `address` and the
`RepoListStore` lookup with its silent early-return is gone. An inbox row
opens whether or not the repository is in the list yet.
3. `open_repo_panel` and `RepoDetailView::new` take an address plus an optional
hint, so a repository panel opens from a `RepoAddr` alone. This pulls the
address-based constructor forward from Phase 3 step 2.
4. `RepoDetailView::attach_store` adopts the store's first announcement when
`initial` is still empty and calls `load_repo`, so a panel opened by address
fills in instead of waiting for the caller to have the announcement.
5. `open_upstream`: the 60 x 250 ms poll and the `pending_upstream` field are
gone. It opens the panel by address; the store's `subscribe_remote` fetches
the announcement from the bootstrap relays and step 4 loads the explorer.
### Phase 3 - one entity for local and NIP-34
1. `signed_state/src/repo.rs`: `addr`/`path` options, `new_local`,
`announce`, `Option<Subscription>`, action guards.
2. `views/repo/mod.rs`: single `store` field; `new`/`new_local`; header,
display name, `load_repo`, `open_init_dialog` derive from the store.
2. `views/repo/mod.rs`: single `store` field; `new_local`; header,
display name, `load_repo`, `open_init_dialog` derive from the store. The
address-based `new` is already in place from Phase 2.
3. `views/repo/store.rs`: always observe; `refresh_statuses` returns false
when not announced.
4. `views/repo/actions.rs`, `header.rs`, `banners.rs`: drop