update checkout store
This commit is contained in:
@@ -1836,10 +1836,8 @@ impl RepoDetailView {
|
||||
fn attach_store(&mut self, store: &Entity<RepoStore>, cx: &mut Context<Self>) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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<DockArea>,
|
||||
explore: Option<WeakEntity<RepoListView>>,
|
||||
logged_in: bool,
|
||||
/// Repositories the current user announced, listed under the All Repositories heading.
|
||||
/// Recreated when the signer changes.
|
||||
my_repos: Option<Entity<RepoListStore>>,
|
||||
repos: Option<Entity<RepoListStore>>,
|
||||
/// Observes the current user's repo store so the list re-renders.
|
||||
my_repos_subscription: Option<Subscription>,
|
||||
repos_subscription: Option<Subscription>,
|
||||
/// 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<DockArea>, cx: &mut Context<Self>) -> 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>) {
|
||||
self.my_repos_subscription = None;
|
||||
fn refresh_repos(&mut self, cx: &mut Context<Self>) {
|
||||
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<Coordinate> = 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<Self>) {
|
||||
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<Self>) -> impl IntoElement {
|
||||
let store = self.my_repos.as_ref();
|
||||
fn render_repos(&self, cx: &mut Context<Self>) -> 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<String> =
|
||||
announcements.iter().map(|a| a.id.clone()).collect();
|
||||
let ids: HashSet<String> = announcements.iter().map(|a| a.id.clone()).collect();
|
||||
|
||||
let local_repos: Vec<PathBuf> = 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<usize>, _window, cx| {
|
||||
cx.processor(move |this, range: Range<usize>, _, 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<Self>,
|
||||
) -> 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<Self>) -> 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<Self>) -> 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()
|
||||
|
||||
@@ -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))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user