diff --git a/crates/signed_core/src/model.rs b/crates/signed_core/src/model.rs index d977d7e..116dce5 100644 --- a/crates/signed_core/src/model.rs +++ b/crates/signed_core/src/model.rs @@ -271,6 +271,11 @@ impl Announcement { crate::repo_addr(self.owner, self.id.clone()) } + /// The name of the repository, or a default if none is provided. + pub fn name(&self) -> String { + self.name.clone().unwrap_or("Untitled".into()) + } + /// Whether this announcement is a fork of the repository at `base`. /// Its `u` tag points at `base`, which also covers permanent forks whose EUC diverged. /// diff --git a/crates/signed_state/src/checkouts.rs b/crates/signed_state/src/checkouts.rs index c73fca7..c078da8 100644 --- a/crates/signed_state/src/checkouts.rs +++ b/crates/signed_state/src/checkouts.rs @@ -122,7 +122,6 @@ impl CheckoutsStore { })); // Another identity's repositories must not keep the old statuses alive. - // Their polls stop too. subscriptions.push(cx.subscribe(&backend, |this, _backend, event, cx| { if matches!(event, BackendEvent::SignerChanged) { this.status_requested.clear(); @@ -246,7 +245,6 @@ impl CheckoutsStore { let task = cx.spawn(async move |this, cx| { cx.background_executor().timer(REFRESH_DEBOUNCE).await; - this.update(cx, |this, cx| this.run_refresh(cx)) }); diff --git a/crates/signed_state/src/lib.rs b/crates/signed_state/src/lib.rs index 9b9497d..5bd40ac 100644 --- a/crates/signed_state/src/lib.rs +++ b/crates/signed_state/src/lib.rs @@ -30,9 +30,7 @@ pub fn init( cx: &mut App, ) -> Entity { // rustls uses the `aws_lc_rs` provider by default. - rustls::crypto::aws_lc_rs::default_provider() - .install_default() - .ok(); + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); let (client, signer) = cx.foreground_executor().block_on(async move { let path = db_path.as_ref().to_path_buf(); @@ -44,9 +42,7 @@ pub fn init( let entity = cx.new(|cx| Backend::new(client, signer, cx)); Backend::set_global(entity.clone(), cx); ProfileStore::set_global(cx.new(ProfileStore::new), cx); - RepoListStore::set_global(cx.new(|cx| RepoListStore::new(None, cx)), cx); - // The local git clone cache, the grasp mirrors. GitStore::set_global(repos_root, cx); LocalReposStore::set_global(cx.new(|cx| LocalReposStore::new(scan_paths, cx)), cx); CheckoutsStore::set_global(cx.new(CheckoutsStore::new), cx); diff --git a/crates/workspace/src/views/repo_detail/mod.rs b/crates/workspace/src/views/repo_detail/mod.rs index f83636f..06c82ed 100644 --- a/crates/workspace/src/views/repo_detail/mod.rs +++ b/crates/workspace/src/views/repo_detail/mod.rs @@ -1836,10 +1836,8 @@ impl RepoDetailView { fn attach_store(&mut self, store: &Entity, cx: &mut Context) { self._subscriptions .push(cx.observe(store, |this, _store, cx| { - cx.notify(); - // The first refresh fills the announced HEAD. - // It defaults the banner's base branch, re-request when it changes. this.refresh_ready_statuses(cx); + cx.notify(); })); self.refresh_ready_statuses(cx); } diff --git a/crates/workspace/src/views/sidebar/mod.rs b/crates/workspace/src/views/sidebar/mod.rs index 18e6522..fea1f11 100644 --- a/crates/workspace/src/views/sidebar/mod.rs +++ b/crates/workspace/src/views/sidebar/mod.rs @@ -13,14 +13,16 @@ use gpui::{ SharedString, Subscription, WeakEntity, Window, div, img, px, uniform_list, }; use gpui_base::Button as BaseButton; +use gpui_component::badge::Badge; use gpui_component::button::{Button, ButtonVariants}; use gpui_component::input::InputState; use gpui_component::{ActiveTheme, Icon, IconName, Sizable, StyledExt, h_flex, v_flex}; +use nostr::nips::nip01::Coordinate; use signed_core::{Announcement, identifier_from_name}; use signed_state::{ Backend, BackendEvent, CheckoutsStore, LocalReposStore, Profile, ProfileStore, RepoListStore, }; -use signed_ui::{CountBadge, NavItem, PixelAvatar, UserAvatar, title_bar_drag_handlers}; +use signed_ui::{NavItem, PixelAvatar, UserAvatar, title_bar_drag_handlers}; use super::{RepoDetailView, RepoListView, open_repo_panel}; @@ -33,32 +35,22 @@ mod settings_dialog; use self::onboarding_dialog::OnboardingState; -/// Left-dock panel with navigation entries. -/// Entries open content panels in the dock area. pub struct SidebarPanel { focus_handle: FocusHandle, dock_area: WeakEntity, explore: Option>, logged_in: bool, /// Repositories the current user announced, listed under the All Repositories heading. - /// Recreated when the signer changes. - my_repos: Option>, + repos: Option>, /// Observes the current user's repo store so the list re-renders. - my_repos_subscription: Option, + repos_subscription: Option, /// Banner artwork behind the sign-in screen. - /// Picked at random from the bundled `backgrounds/` assets. banner: SharedString, - /// Observes the local-repository scan so new discoveries re-render. - _local_repos_subscription: Subscription, - /// Observes the checkouts store. - /// Its ready-to-push statuses feed the badges on the user's repo rows. - _checkouts_subscription: Subscription, _subscription: Subscription, } impl SidebarPanel { pub fn new(dock_area: WeakEntity, cx: &mut Context) -> Self { - let local_repos_store = LocalReposStore::global(cx); let backend = Backend::global(cx); let logged_in = backend.read(cx).current_user().is_some(); @@ -66,70 +58,60 @@ impl SidebarPanel { match event { BackendEvent::SignerChanged => { this.logged_in = backend.read(cx).current_user().is_some(); - this.refresh_my_repos(cx); + this.refresh_repos(cx); } BackendEvent::SignerRequired => { this.logged_in = false; this.banner = pick_banner(); - this.my_repos = None; - this.my_repos_subscription = None; + this.repos = None; + this.repos_subscription = None; } _ => return, } cx.notify(); }); - let local_repos_subscription = cx.observe(&local_repos_store, |_, _, cx| { - cx.notify(); - }); - - let checkouts_store = CheckoutsStore::global(cx); - let checkouts_subscription = cx.observe(&checkouts_store, |_, _, cx| { - cx.notify(); - }); - let mut panel = Self { focus_handle: cx.focus_handle(), dock_area, logged_in, explore: None, - my_repos: None, - my_repos_subscription: None, + repos: None, + repos_subscription: None, banner: pick_banner(), - _local_repos_subscription: local_repos_subscription, - _checkouts_subscription: checkouts_subscription, _subscription: subscription, }; if logged_in { - panel.refresh_my_repos(cx); + panel.refresh_repos(cx); + cx.notify(); } panel } /// Recreate the store listing the current user's repositories. + /// /// Watch each repository for unpushed local work. - fn refresh_my_repos(&mut self, cx: &mut Context) { - self.my_repos_subscription = None; + fn refresh_repos(&mut self, cx: &mut Context) { + self.repos_subscription = None; + let checkouts = CheckoutsStore::global(cx); let backend = Backend::global(cx); let author = backend.read(cx).current_user(); - self.my_repos = author.map(|author| cx.new(|cx| RepoListStore::new(Some(author), cx))); - if let Some(store) = self.my_repos.as_ref() { - self.my_repos_subscription = Some(cx.observe(store, |_this, store, cx| { - cx.notify(); - // These are the signed-in user's own repositories. - // Request their ready-to-push statuses, deduplicated per repository. - // The rows carry a badge while local work is unpushed. - let addrs: Vec<_> = store + // Create a new repo list store for the signed-in user, if they are logged in. + self.repos = author.map(|author| cx.new(|cx| RepoListStore::new(Some(author), cx))); + + if let Some(store) = self.repos.as_ref() { + self.repos_subscription = Some(cx.observe(store, move |_, store, cx| { + let addrs: Vec = store .read(cx) .announcements .iter() .map(|a| a.addr()) .collect(); - let checkouts = CheckoutsStore::global(cx); + checkouts.update(cx, |checkouts, cx| { for addr in addrs { checkouts.request_push_statuses(&addr, cx); @@ -193,22 +175,21 @@ impl SidebarPanel { } /// Open a local repository's detail view in the dock's center. + /// /// The detail view offers to publish it to NIP-34. fn open_local_repo(&mut self, path: PathBuf, window: &mut Window, cx: &mut Context) { let detail = cx.new(|cx| RepoDetailView::new_local(self.dock_area.clone(), path, window, cx)); - let _ = self.dock_area.update(cx, |dock_area, cx| { - add_center_panel(dock_area, panel_handle(detail), window, cx); - }); + self.dock_area + .update(cx, |dock_area, cx| { + add_center_panel(dock_area, panel_handle(detail), window, cx); + }) + .ok(); } - /// The All Repositories section of the sidebar. - /// A header with the create button above the current user's repositories. - /// Rendered lazily through a [`uniform_list`]. - /// Followed by local git repositories from the startup scan. - fn render_my_repos(&self, cx: &mut Context) -> impl IntoElement { - let store = self.my_repos.as_ref(); + fn render_repos(&self, cx: &mut Context) -> impl IntoElement { + let store = self.repos.as_ref(); let local = LocalReposStore::global(cx); let local_repos = local.read(cx).repos.clone(); let scanning = local.read(cx).scanning; @@ -260,22 +241,19 @@ impl SidebarPanel { ) .when_some(store, |builder, store| { let announcements = store.read(cx).announcements.clone(); - // Local repositories already published to NIP-34 appear above. - // Hide them from the local section here. - // Matched by the identifier derived from the directory name. - // Same derivation as the init dialog's default name. - let announced_ids: HashSet = - announcements.iter().map(|a| a.id.clone()).collect(); + let ids: HashSet = announcements.iter().map(|a| a.id.clone()).collect(); + let local_repos: Vec = local_repos .iter() .filter(|path| { let Some(name) = path.file_name() else { return true; }; - !announced_ids.contains(&identifier_from_name(&name.to_string_lossy())) + !ids.contains(&identifier_from_name(&name.to_string_lossy())) }) .cloned() .collect(); + // One merged list, the user's NIP-34 repositories first. // Local repositories discovered by the scan follow. let total = announcements.len() + local_repos.len(); @@ -288,10 +266,12 @@ impl SidebarPanel { .py_1() .text_xs() .text_color(cx.theme().muted_foreground) - .child(if scanning { - "Scanning for local repositories…" - } else { - "No repositories yet" + .map(|this| { + if scanning { + this.child("Scanning for local repositories…") + } else { + this.child("No repositories yet") + } }), ) } else { @@ -299,16 +279,11 @@ impl SidebarPanel { uniform_list( "repos", total, - cx.processor(move |this, range: Range, _window, cx| { + cx.processor(move |this, range: Range, _, cx| { range .map(|ix| { - this.render_repo_row_at( - &announcements, - &local_repos, - ix, - cx, - ) - .into_any_element() + this.render_repo_at(&announcements, &local_repos, ix, cx) + .into_any_element() }) .collect() }), @@ -321,7 +296,7 @@ impl SidebarPanel { } /// One row of the merged sidebar list, a NIP-34 or a local repository. - fn render_repo_row_at( + fn render_repo_at( &self, announcements: &[Announcement], local_repos: &[PathBuf], @@ -345,27 +320,24 @@ impl SidebarPanel { announcement: &Announcement, cx: &mut Context, ) -> impl IntoElement { - let name = announcement - .name - .as_deref() - .map(SharedString::from) - .unwrap_or_else(|| SharedString::from(announcement.id.clone())); + let name = announcement.name().map(SharedString::from); let avatar = PixelAvatar::new(format!("{}:{}", announcement.owner, announcement.id)); + let announcement = announcement.clone(); // Badge with the unpushed commit count of the repository's local checkouts. // The commits are ready to push to the grasp servers. - let unpushed: usize = CheckoutsStore::global(cx) + let checkout = CheckoutsStore::global(cx); + let unpushed = checkout .read(cx) .push_statuses_of(&announcement.addr()) .iter() .map(|status| status.ahead as usize) .sum(); - let announcement = announcement.clone(); - let mut row = NavItem::new(format!("my-repo:{}", announcement.id), name, avatar); + let mut row = NavItem::new(format!("repo:{}", announcement.id), name, avatar); if unpushed > 0 { - row = row.suffix(CountBadge::new(unpushed)); + row = row.suffix(Badge::new().count(unpushed).xsmall()); } row.on_click( @@ -373,29 +345,26 @@ impl SidebarPanel { ) } - /// One local repository row, a deterministic pixel avatar seeded from the path. + /// One local repository row. + /// /// The directory name and a warning suffix, the repo is not yet set up for NIP-34. - /// Clicking opens the detail view, which offers to initialize it. fn render_local_row(&self, path: &Path, cx: &mut Context) -> impl IntoElement { let name = path .file_name() .map(|name| name.to_string_lossy().into_owned()) .unwrap_or_else(|| path.display().to_string()); let path = path.to_path_buf(); + let avatar = PixelAvatar::new(path.to_string_lossy()); - NavItem::new( - format!("local-repo:{}", path.display()), - name, - PixelAvatar::new(path.to_string_lossy()), - ) - .suffix( - Icon::new(IconName::TriangleAlert) - .small() - .text_color(cx.theme().warning), - ) - .on_click(cx.listener(move |this, _ev, window, cx| { - this.open_local_repo(path.clone(), window, cx); - })) + NavItem::new(format!("local-repo:{}", path.display()), name, avatar) + .suffix( + Icon::new(IconName::TriangleAlert) + .small() + .text_color(cx.theme().warning), + ) + .on_click(cx.listener(move |this, _ev, window, cx| { + this.open_local_repo(path.clone(), window, cx); + })) } /// Show the Import Identity dialog. @@ -434,7 +403,6 @@ impl SidebarPanel { } /// Sign-in placeholder shown while logged out. - /// Banner artwork behind a scrim keeps the CTA buttons readable in both themes. fn render_sign_in(&self, window: &mut Window, cx: &mut Context) -> Div { v_flex() .size_full() @@ -598,7 +566,7 @@ impl Render for SidebarPanel { )), ), ) - .child(self.render_my_repos(cx)), + .child(self.render_repos(cx)), ) .child( v_flex() diff --git a/crates/workspace/src/views/sidebar/settings_dialog.rs b/crates/workspace/src/views/sidebar/settings_dialog.rs index c365c88..b8c13ad 100644 --- a/crates/workspace/src/views/sidebar/settings_dialog.rs +++ b/crates/workspace/src/views/sidebar/settings_dialog.rs @@ -267,23 +267,14 @@ impl SettingsControls { /// Open the Settings dialog. pub fn open(window: &mut Window, cx: &mut App) { let controls = Rc::new(SettingsControls::new(window, cx)); - let store = SettingsStore::global(cx); - let window_handle = window.window_handle(); - let store_subscription = cx.observe(&store, move |_, cx| { - window_handle - .update(cx, |_, window, _| window.refresh()) - .ok(); - }); - - let dialog_state = Rc::new((controls, store_subscription)); window.open_dialog(cx, move |dialog, _window, cx| { - let dialog_state = dialog_state.clone(); + let controls = controls.clone(); dialog .title("Settings") .width(px(650.)) .h(px(560.)) - .child(settings_view(&dialog_state.0, cx)) + .child(settings_view(&controls, cx)) }); }