From 025fd15837ff4267eebd919a7c81e0c450944cbe Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Sun, 13 Sep 2026 12:20:49 +0700 Subject: [PATCH] refactor 2 --- crates/workspace/src/views/inbox.rs | 12 +- crates/workspace/src/views/repo/actions.rs | 119 ++++++------------ .../workspace/src/views/repo/init_dialog.rs | 2 +- crates/workspace/src/views/repo/mod.rs | 33 +++-- crates/workspace/src/views/repo/store.rs | 27 +++- crates/workspace/src/views/repo_list.rs | 8 +- .../src/views/sidebar/create_repo_dialog.rs | 8 +- crates/workspace/src/views/sidebar/mod.rs | 8 +- docs/repo-state-plan.md | 35 ++++-- 9 files changed, 124 insertions(+), 128 deletions(-) diff --git a/crates/workspace/src/views/inbox.rs b/crates/workspace/src/views/inbox.rs index 01d7fee..172a26c 100644 --- a/crates/workspace/src/views/inbox.rs +++ b/crates/workspace/src/views/inbox.rs @@ -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) -> AnyElement { diff --git a/crates/workspace/src/views/repo/actions.rs b/crates/workspace/src/views/repo/actions.rs index 095fec5..8d35f94 100644 --- a/crates/workspace/src/views/repo/actions.rs +++ b/crates/workspace/src/views/repo/actions.rs @@ -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) { - 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> = 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, - announcement: &Announcement, + addr: &RepoAddr, + hint: Option<&Announcement>, window: &mut Window, cx: &mut App, ) -> Entity { - 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 { - 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 { + 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, - 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 = 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) })) diff --git a/crates/workspace/src/views/repo/init_dialog.rs b/crates/workspace/src/views/repo/init_dialog.rs index 02083bf..8187111 100644 --- a/crates/workspace/src/views/repo/init_dialog.rs +++ b/crates/workspace/src/views/repo/init_dialog.rs @@ -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); }); } }) diff --git a/crates/workspace/src/views/repo/mod.rs b/crates/workspace/src/views/repo/mod.rs index 5607fce..18edd49 100644 --- a/crates/workspace/src/views/repo/mod.rs +++ b/crates/workspace/src/views/repo/mod.rs @@ -73,7 +73,8 @@ pub struct RepoDetailView { dock_area: WeakEntity, /// 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, /// 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, - /// Upstream repository, from this fork's `u` tag, the user asked to open. - /// Its announcement is still being fetched. - pending_upstream: Option, } 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, - initial: Announcement, + addr: RepoAddr, + hint: Option, window: &mut Window, cx: &mut Context, ) -> 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, } diff --git a/crates/workspace/src/views/repo/store.rs b/crates/workspace/src/views/repo/store.rs index 2bc4e38..9279200 100644 --- a/crates/workspace/src/views/repo/store.rs +++ b/crates/workspace/src/views/repo/store.rs @@ -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, ) { // 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, cx: &mut Context) { + pub(super) fn attach_store( + &mut self, + store: &Entity, + window: &mut Window, + cx: &mut Context, + ) { 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); diff --git a/crates/workspace/src/views/repo_list.rs b/crates/workspace/src/views/repo_list.rs index c3dec27..b3a6593 100644 --- a/crates/workspace/src/views/repo_list.rs +++ b/crates/workspace/src/views/repo_list.rs @@ -189,7 +189,13 @@ impl RepoListView { window: &mut Window, cx: &mut Context, ) { - open_repo_panel(&self.dock_area, announcement, window, cx); + open_repo_panel( + &self.dock_area, + &announcement.addr(), + Some(announcement), + window, + cx, + ); } fn render_card( diff --git a/crates/workspace/src/views/sidebar/create_repo_dialog.rs b/crates/workspace/src/views/sidebar/create_repo_dialog.rs index a4723f0..9353446 100644 --- a/crates/workspace/src/views/sidebar/create_repo_dialog.rs +++ b/crates/workspace/src/views/sidebar/create_repo_dialog.rs @@ -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, + ); } diff --git a/crates/workspace/src/views/sidebar/mod.rs b/crates/workspace/src/views/sidebar/mod.rs index 950e790..c660e16 100644 --- a/crates/workspace/src/views/sidebar/mod.rs +++ b/crates/workspace/src/views/sidebar/mod.rs @@ -260,7 +260,13 @@ impl SidebarPanel { window: &mut Window, cx: &mut Context, ) { - 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. diff --git a/docs/repo-state-plan.md b/docs/repo-state-plan.md index bd4deee..5ab7777 100644 --- a/docs/repo-state-plan.md +++ b/docs/repo-state-plan.md @@ -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`, 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