Compare commits

...
2 Commits
Author SHA1 Message Date
reya 7ff09502b9 update 2026-09-05 15:27:35 +07:00
reya 1f3afea81b update checkout store 2026-09-04 20:13:10 +07:00
9 changed files with 360 additions and 272 deletions
+1
View File
@@ -105,6 +105,7 @@ impl SignedDockSkin {
} }
/// Payload a dock's resize handle drags. /// Payload a dock's resize handle drags.
///
/// It draws nothing, the handle element is the visible affordance. /// It draws nothing, the handle element is the visible affordance.
#[derive(Clone)] #[derive(Clone)]
struct ResizePanel; struct ResizePanel;
-7
View File
@@ -94,13 +94,6 @@ pub fn comments_for(roots: impl IntoIterator<Item = EventId>) -> Vec<Filter> {
] ]
} }
/// All repositories announced by an author.
pub fn announcements_by(public_key: PublicKey) -> Filter {
Filter::new()
.kind(Kind::GitRepoAnnouncement)
.author(public_key)
}
/// All repository announcements, for global discovery. /// All repository announcements, for global discovery.
pub fn all_announcements() -> Filter { pub fn all_announcements() -> Filter {
Filter::new().kind(Kind::GitRepoAnnouncement) Filter::new().kind(Kind::GitRepoAnnouncement)
+8 -3
View File
@@ -2,7 +2,7 @@ use std::collections::HashSet;
use nostr::prelude::*; use nostr::prelude::*;
use crate::RepoAddr; use crate::{RepoAddr, repo_addr};
/// Parsed NIP-34 repository announcement, plain data ready for the UI. /// Parsed NIP-34 repository announcement, plain data ready for the UI.
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@@ -267,8 +267,13 @@ impl Announcement {
} }
/// The repository address of this announcement. /// The repository address of this announcement.
pub fn addr(&self) -> crate::RepoAddr { pub fn addr(&self) -> RepoAddr {
crate::repo_addr(self.owner, self.id.clone()) 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`. /// Whether this announcement is a fork of the repository at `base`.
+34 -10
View File
@@ -64,10 +64,17 @@ struct Remembered {
} }
/// Global store of local-checkout associations and per-checkout statuses. /// Global store of local-checkout associations and per-checkout statuses.
///
/// Readers (the sidebar rows, the repository panels) observe this store and
/// derive what they display from their own snapshots, so publishing needs no
/// fine-grained entities: the store notifies when a slice changed and each
/// reader re-derives only what it shows.
pub struct CheckoutsStore { pub struct CheckoutsStore {
/// Checkout paths per announced repository. /// Checkout paths per announced repository.
by_repo: HashMap<RepoAddr, Vec<PathBuf>>, by_repo: HashMap<RepoAddr, Vec<PathBuf>>,
/// Ready-to-contribute statuses of the requested repositories. /// Ready-to-contribute statuses of the requested repositories.
///
/// Those are the repository detail panels currently open.
statuses: HashMap<RepoAddr, Vec<CheckoutStatus>>, statuses: HashMap<RepoAddr, Vec<CheckoutStatus>>,
/// Repositories whose statuses are recomputed on every input change. /// Repositories whose statuses are recomputed on every input change.
/// ///
@@ -77,7 +84,7 @@ pub struct CheckoutsStore {
/// ///
/// The sidebar rows of the user's own repositories and their detail panels. /// The sidebar rows of the user's own repositories and their detail panels.
push_requested: HashSet<RepoAddr>, push_requested: HashSet<RepoAddr>,
/// Ready-to-push statuses of the requested own repositories. /// The ready-to-push statuses of the requested own repositories.
push_statuses: HashMap<RepoAddr, Vec<CheckoutStatus>>, push_statuses: HashMap<RepoAddr, Vec<CheckoutStatus>>,
/// Last announced head branch per requested repository. /// Last announced head branch per requested repository.
/// ///
@@ -85,8 +92,8 @@ pub struct CheckoutsStore {
requested_head: HashMap<RepoAddr, Option<String>>, requested_head: HashMap<RepoAddr, Option<String>>,
/// Refresh coalescing, see [`RefreshGate`]. /// Refresh coalescing, see [`RefreshGate`].
refresh: RefreshGate, refresh: RefreshGate,
_subscriptions: Vec<Subscription>,
tasks: Vec<Task<Result<(), Error>>>, tasks: Vec<Task<Result<(), Error>>>,
_subscriptions: Vec<Subscription>,
} }
impl CheckoutsStore { impl CheckoutsStore {
@@ -122,14 +129,14 @@ impl CheckoutsStore {
})); }));
// Another identity's repositories must not keep the old statuses alive. // Another identity's repositories must not keep the old statuses alive.
// Their polls stop too.
subscriptions.push(cx.subscribe(&backend, |this, _backend, event, cx| { subscriptions.push(cx.subscribe(&backend, |this, _backend, event, cx| {
if matches!(event, BackendEvent::SignerChanged) { if matches!(event, BackendEvent::SignerChanged) {
this.status_requested.clear(); this.status_requested.clear();
this.push_requested.clear(); this.push_requested.clear();
this.requested_head.clear(); this.requested_head.clear();
this.statuses = HashMap::new(); this.statuses.clear();
this.push_statuses = HashMap::new(); this.push_statuses.clear();
cx.notify();
this.refresh(cx); this.refresh(cx);
} }
})); }));
@@ -143,8 +150,8 @@ impl CheckoutsStore {
push_statuses: HashMap::new(), push_statuses: HashMap::new(),
requested_head: HashMap::new(), requested_head: HashMap::new(),
refresh: RefreshGate::default(), refresh: RefreshGate::default(),
_subscriptions: subscriptions,
tasks: Vec::new(), tasks: Vec::new(),
_subscriptions: subscriptions,
}; };
if !cfg!(target_arch = "wasm32") { if !cfg!(target_arch = "wasm32") {
@@ -218,7 +225,7 @@ impl CheckoutsStore {
/// The ready-to-contribute statuses of `addr`. /// The ready-to-contribute statuses of `addr`.
/// ///
/// Empty while none are known or nothing is ahead. /// Empty while none are known or nothing is ahead.
pub fn statuses_of(&self, addr: &RepoAddr) -> Vec<CheckoutStatus> { pub fn ready_statuses_of(&self, addr: &RepoAddr) -> Vec<CheckoutStatus> {
self.statuses.get(addr).cloned().unwrap_or_default() self.statuses.get(addr).cloned().unwrap_or_default()
} }
@@ -229,13 +236,20 @@ impl CheckoutsStore {
} }
/// The ready-to-push statuses of `addr`. /// The ready-to-push statuses of `addr`.
/// Only meaningful for repositories announced by the signed-in user.
/// ///
/// Empty while none are known or nothing is unpushed. /// Empty while none are known or nothing is unpushed.
pub fn push_statuses_of(&self, addr: &RepoAddr) -> Vec<CheckoutStatus> { pub fn push_statuses_of(&self, addr: &RepoAddr) -> Vec<CheckoutStatus> {
self.push_statuses.get(addr).cloned().unwrap_or_default() self.push_statuses.get(addr).cloned().unwrap_or_default()
} }
/// The number of unpushed commits for a repository.
pub fn unpushed(&self, addr: &RepoAddr) -> usize {
self.push_statuses
.get(addr)
.map(|list| list.iter().map(|status| status.ahead as usize).sum())
.unwrap_or(0)
}
/// Re-resolve the associations and the requested statuses. /// Re-resolve the associations and the requested statuses.
/// ///
/// Requests arriving while a pass runs fold into a follow-up. /// Requests arriving while a pass runs fold into a follow-up.
@@ -246,7 +260,6 @@ impl CheckoutsStore {
let task = cx.spawn(async move |this, cx| { let task = cx.spawn(async move |this, cx| {
cx.background_executor().timer(REFRESH_DEBOUNCE).await; cx.background_executor().timer(REFRESH_DEBOUNCE).await;
this.update(cx, |this, cx| this.run_refresh(cx)) this.update(cx, |this, cx| this.run_refresh(cx))
}); });
@@ -364,10 +377,20 @@ impl CheckoutsStore {
}; };
let again = this.update(cx, |this, cx| { let again = this.update(cx, |this, cx| {
let associations_changed = this.by_repo != associations;
let statuses_changed = this.statuses != statuses;
let push_statuses_changed = this.push_statuses != push_statuses;
this.by_repo = associations; this.by_repo = associations;
this.statuses = statuses; this.statuses = statuses;
this.push_statuses = push_statuses; this.push_statuses = push_statuses;
cx.notify();
// Poll cycles and identity re-requests recompute the same maps
// over and over. Notify only when something actually changed,
// so observers skip the no-op heartbeats.
if associations_changed || statuses_changed || push_statuses_changed {
cx.notify();
}
this.refresh.finish() this.refresh.finish()
})?; })?;
@@ -380,6 +403,7 @@ impl CheckoutsStore {
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
if poll && this.refresh.idle() { if poll && this.refresh.idle() {
this.refresh.debounce(); this.refresh.debounce();
// Open panels get the fast cadence. // Open panels get the fast cadence.
// Each cycle fetches every watched checkout's remote. // Each cycle fetches every watched checkout's remote.
let delay = if this.status_requested.is_empty() { let delay = if this.status_requested.is_empty() {
+3 -7
View File
@@ -30,9 +30,7 @@ pub fn init(
cx: &mut App, cx: &mut App,
) -> Entity<Backend> { ) -> Entity<Backend> {
// rustls uses the `aws_lc_rs` provider by default. // rustls uses the `aws_lc_rs` provider by default.
rustls::crypto::aws_lc_rs::default_provider() let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
.install_default()
.ok();
let (client, signer) = cx.foreground_executor().block_on(async move { let (client, signer) = cx.foreground_executor().block_on(async move {
let path = db_path.as_ref().to_path_buf(); 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)); let entity = cx.new(|cx| Backend::new(client, signer, cx));
Backend::set_global(entity.clone(), cx); Backend::set_global(entity.clone(), cx);
ProfileStore::set_global(cx.new(ProfileStore::new), cx); ProfileStore::set_global(cx.new(ProfileStore::new), cx);
RepoListStore::set_global(cx.new(RepoListStore::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); GitStore::set_global(repos_root, cx);
LocalReposStore::set_global(cx.new(|cx| LocalReposStore::new(scan_paths, cx)), cx); LocalReposStore::set_global(cx.new(|cx| LocalReposStore::new(scan_paths, cx)), cx);
CheckoutsStore::set_global(cx.new(CheckoutsStore::new), cx); CheckoutsStore::set_global(cx.new(CheckoutsStore::new), cx);
@@ -61,7 +57,7 @@ pub fn init(cx: &mut App) -> Entity<Backend> {
let entity = cx.new(|cx| Backend::new(client, signer, cx)); let entity = cx.new(|cx| Backend::new(client, signer, cx));
Backend::set_global(entity.clone(), cx); Backend::set_global(entity.clone(), cx);
ProfileStore::set_global(cx.new(ProfileStore::new), cx); ProfileStore::set_global(cx.new(ProfileStore::new), cx);
RepoListStore::set_global(cx.new(|cx| RepoListStore::new(None, cx)), cx); RepoListStore::set_global(cx.new(RepoListStore::new), cx);
GitStore::set_global(PathBuf::new(), cx); GitStore::set_global(PathBuf::new(), cx);
LocalReposStore::set_global(cx.new(|cx| LocalReposStore::new(Vec::new(), cx)), cx); LocalReposStore::set_global(cx.new(|cx| LocalReposStore::new(Vec::new(), cx)), cx);
CheckoutsStore::set_global(cx.new(|cx| CheckoutsStore::new(cx)), cx); CheckoutsStore::set_global(cx.new(|cx| CheckoutsStore::new(cx)), cx);
+18 -23
View File
@@ -42,7 +42,7 @@ impl RepoActivityCounts {
} }
} }
/// Store listing repository announcements, global discovery or per-author. /// Store listing the discovered repository announcements, newest first.
pub struct RepoListStore { pub struct RepoListStore {
/// Shared so views can clone the list per frame without a deep copy. /// Shared so views can clone the list per frame without a deep copy.
pub announcements: Arc<Vec<Announcement>>, pub announcements: Arc<Vec<Announcement>>,
@@ -53,7 +53,6 @@ pub struct RepoListStore {
/// ///
/// Used for the Popular ranking of the explore list. /// Used for the Popular ranking of the explore list.
pub counts: Arc<HashMap<RepoAddr, RepoActivityCounts>>, pub counts: Arc<HashMap<RepoAddr, RepoActivityCounts>>,
author: Option<PublicKey>,
/// Refresh coalescing, see [`RefreshGate`]. /// Refresh coalescing, see [`RefreshGate`].
refresh: RefreshGate, refresh: RefreshGate,
tasks: Vec<Task<Result<(), Error>>>, tasks: Vec<Task<Result<(), Error>>>,
@@ -61,7 +60,7 @@ pub struct RepoListStore {
} }
impl RepoListStore { impl RepoListStore {
/// Retrieve the global explore store. /// Retrieve the global repository list store.
pub fn global(cx: &App) -> Entity<Self> { pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalRepoListStore>().0.clone() cx.global::<GlobalRepoListStore>().0.clone()
} }
@@ -70,8 +69,8 @@ impl RepoListStore {
cx.set_global(GlobalRepoListStore(entity)); cx.set_global(GlobalRepoListStore(entity));
} }
/// Create a store. If `author` is `None`, all announcements are listed. /// Create the store listing all announcements.
pub fn new(author: Option<PublicKey>, cx: &mut Context<Self>) -> Self { pub fn new(cx: &mut Context<Self>) -> Self {
let backend = Backend::global(cx); let backend = Backend::global(cx);
let subscription = cx.subscribe(&backend, |this, _backend, event, cx| { let subscription = cx.subscribe(&backend, |this, _backend, event, cx| {
@@ -87,14 +86,11 @@ impl RepoListStore {
} else { } else {
let is_announcement = update.kind == Kind::GitRepoAnnouncement; let is_announcement = update.kind == Kind::GitRepoAnnouncement;
let is_repo_state = update.kind == Kind::RepoState; let is_repo_state = update.kind == Kind::RepoState;
let tracked = is_announcement || is_repo_state; is_announcement || is_repo_state
tracked && this.author.is_none_or(|a| a == update.author)
} }
} }
BackendEvent::Published(event) => { BackendEvent::Published(event) => {
let kind_match = event.kind == Kind::GitRepoAnnouncement; let announcement = event.kind == Kind::GitRepoAnnouncement;
let author_match = this.author.is_none_or(|a| a == event.pubkey);
let announcement = kind_match && author_match;
// Locally published deletions are already in the local database. // Locally published deletions are already in the local database.
// Refresh so they take effect immediately, like relay deletions. // Refresh so they take effect immediately, like relay deletions.
@@ -116,7 +112,6 @@ impl RepoListStore {
announcements: Arc::new(Vec::new()), announcements: Arc::new(Vec::new()),
last_activity: Arc::new(HashMap::new()), last_activity: Arc::new(HashMap::new()),
counts: Arc::new(HashMap::new()), counts: Arc::new(HashMap::new()),
author,
refresh: RefreshGate::default(), refresh: RefreshGate::default(),
_subscription: subscription, _subscription: subscription,
tasks: Vec::new(), tasks: Vec::new(),
@@ -129,6 +124,15 @@ impl RepoListStore {
store store
} }
/// The announcements of `user`, newest first.
pub fn announcements_of(&self, user: &PublicKey) -> Vec<Announcement> {
self.announcements
.iter()
.filter(|a| a.owner == *user)
.cloned()
.collect()
}
/// Track a spawned task, pruning finished tasks first. /// Track a spawned task, pruning finished tasks first.
/// ///
/// Keeps the store's task list bounded by the number of in-flight tasks. /// Keeps the store's task list bounded by the number of in-flight tasks.
@@ -140,14 +144,9 @@ impl RepoListStore {
/// Negentropy-sync announcements with the bootstrap relays. /// Negentropy-sync announcements with the bootstrap relays.
fn subscribe_remote(&mut self, cx: &mut Context<Self>) { fn subscribe_remote(&mut self, cx: &mut Context<Self>) {
let backend = Backend::global(cx); let backend = Backend::global(cx);
let author = self.author;
backend.update(cx, |backend, cx| { backend.update(cx, |backend, cx| {
let filter = match author { backend.sync_bootstrap(filters::all_announcements(), cx);
Some(a) => filters::announcements_by(a),
None => filters::all_announcements(),
};
backend.sync_bootstrap(filter, cx);
// Deletion requests, NIP-09/62, must be known before any announcement is shown. // Deletion requests, NIP-09/62, must be known before any announcement is shown.
backend.sync_bootstrap(filters::deletions(), cx); backend.sync_bootstrap(filters::deletions(), cx);
}); });
@@ -187,15 +186,11 @@ impl RepoListStore {
let backend = Backend::global(cx); let backend = Backend::global(cx);
let client = backend.read(cx).client(); let client = backend.read(cx).client();
let author = self.author;
let work = cx.background_spawn(async move { let work = cx.background_spawn(async move {
let filter = match author { let filter = filters::all_announcements();
Some(a) => filters::announcements_by(a),
None => filters::all_announcements(),
};
let events = client.database().query(filter).await?; let events = client.database().query(filter).await?;
let deletion_events = client.database().query(filters::deletions()).await?; let deletion_events = client.database().query(filters::deletions()).await?;
let deletions = Deletions::from_events(deletion_events); let deletions = Deletions::from_events(deletion_events);
+97 -45
View File
@@ -184,24 +184,31 @@ pub struct RepoDetailView {
tasks: Vec<Task<Result<(), Error>>>, tasks: Vec<Task<Result<(), Error>>>,
/// Subscriptions keeping the selectors' confirm events alive. /// Subscriptions keeping the selectors' confirm events alive.
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
/// Observes the checkouts store.
/// Its statuses feed the ready-to-contribute banner of the repository panel.
_checkouts_subscription: Subscription,
/// `(path, branch)` ready-suggestions dismissed by the user, per panel. /// `(path, branch)` ready-suggestions dismissed by the user, per panel.
banner_dismissed: HashSet<(PathBuf, String)>, banner_dismissed: HashSet<(PathBuf, String)>,
/// The announced HEAD the ready statuses were last requested with. /// The announced HEAD the ready statuses were last requested with.
/// Whether they were requested at all. /// Whether they were requested at all.
/// Re-requested only when the HEAD, the base default, changes. /// Re-requested only when the HEAD, the base default, changes.
/// E.g. when the store's first refresh lands. /// e.g. when the store's first refresh lands.
ready_requested: bool, ready_requested: bool,
ready_head: Option<String>, ready_head: Option<String>,
/// The global checkouts store's ready-to-contribute statuses of this
/// repository, last seen when they drove a render.
///
/// The store notifies on any recompute pass; the observer re-renders this
/// panel only when these slices changed.
ready_statuses: Vec<CheckoutStatus>,
/// 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. /// Upstream repository, from this fork's `u` tag, the user asked to open.
/// Its announcement is still being fetched. /// Its announcement is still being fetched.
pending_upstream: Option<RepoAddr>, pending_upstream: Option<RepoAddr>,
} }
impl RepoDetailView { impl RepoDetailView {
/// Open a repository announced on NIP-34. /// Open a repository announced.
///
/// The store connects to the announcement's relays and loads issues, PRs and statuses. /// The store connects to the announcement's relays and loads issues, PRs and statuses.
pub fn new( pub fn new(
dock_area: WeakEntity<DockArea>, dock_area: WeakEntity<DockArea>,
@@ -228,8 +235,6 @@ impl RepoDetailView {
} }
/// Open a local repository discovered by the scan. /// Open a local repository discovered by the scan.
/// There is no announcement and no nostr store until the user publishes it to NIP-34.
/// The header shows an Init button instead of the NIP-34 actions.
pub fn new_local( pub fn new_local(
dock_area: WeakEntity<DockArea>, dock_area: WeakEntity<DockArea>,
local_path: PathBuf, local_path: PathBuf,
@@ -240,6 +245,7 @@ impl RepoDetailView {
} }
/// Shared construction. /// Shared construction.
///
/// File explorer state, ref selectors and the deferred repository load. /// File explorer state, ref selectors and the deferred repository load.
fn new_common( fn new_common(
dock_area: WeakEntity<DockArea>, dock_area: WeakEntity<DockArea>,
@@ -271,7 +277,7 @@ impl RepoDetailView {
.searchable(true) .searchable(true)
}); });
let subscriptions = vec![ let mut subscriptions = vec![
cx.subscribe_in(&branch_select, window, |this, _state, event, window, cx| { cx.subscribe_in(&branch_select, window, |this, _state, event, window, cx| {
// `Change` fires only when the selection actually changed. // `Change` fires only when the selection actually changed.
// Picking the already-selected branch emits nothing. // Picking the already-selected branch emits nothing.
@@ -291,6 +297,17 @@ impl RepoDetailView {
}), }),
]; ];
// The ready-to-contribute and ready-to-push banners are driven by the
// global checkouts store. It notifies on every recompute; compare the
// statuses of this repository so unrelated updates (the sidebar badges,
// other open panels) do not re-render this panel.
let checkouts = CheckoutsStore::global(cx);
subscriptions.push(cx.observe(&checkouts, |this, _checkouts, cx| {
if this.refresh_statuses(cx) {
cx.notify();
}
}));
// Defer loading the repository until the window is ready. // Defer loading the repository until the window is ready.
cx.defer_in(window, |this, window, cx| { cx.defer_in(window, |this, window, cx| {
this.load_repo(window, cx); this.load_repo(window, cx);
@@ -328,15 +345,15 @@ impl RepoDetailView {
tag_select, tag_select,
switching_ref: false, switching_ref: false,
ref_generation: 0, ref_generation: 0,
focus_handle: cx.focus_handle(),
tasks: Vec::new(),
_subscriptions: subscriptions,
_checkouts_subscription: cx
.observe(&CheckoutsStore::global(cx), |_this, _store, cx| cx.notify()),
banner_dismissed: HashSet::new(), banner_dismissed: HashSet::new(),
ready_requested: false, ready_requested: false,
ready_head: None, ready_head: None,
ready_statuses: Vec::new(),
push_statuses: Vec::new(),
pending_upstream: None, pending_upstream: None,
focus_handle: cx.focus_handle(),
tasks: Vec::new(),
_subscriptions: subscriptions,
} }
} }
@@ -371,16 +388,20 @@ impl RepoDetailView {
})?; })?;
Ok(()) Ok(())
}); });
self.tasks.push(task); self.tasks.push(task);
return; return;
} }
let Some(initial) = self.initial.as_ref() else { let Some(initial) = self.initial.as_ref() else {
return; return;
}; };
let cache = GitStore::global(cx).cache().clone(); let cache = GitStore::global(cx).cache().clone();
let addr = initial.addr(); let addr = initial.addr();
let clone_urls: Vec<String> = initial.clone.iter().map(ToString::to_string).collect(); let clone_urls: Vec<String> = initial.clone.iter().map(ToString::to_string).collect();
// Captured before the loads start. // Captured before the loads start.
// A branch/tag switch bumps the generation, discarding the refresh below. // A branch/tag switch bumps the generation, discarding the refresh below.
let refresh_generation = self.ref_generation; let refresh_generation = self.ref_generation;
@@ -1414,8 +1435,7 @@ impl RepoDetailView {
RepoAction::Delete => this.delete_repository(window, cx), RepoAction::Delete => this.delete_repository(window, cx),
}), }),
) )
.px_4() .p_4()
.pb_4()
.w_full() .w_full()
.gap_8() .gap_8()
.border_b_1() .border_b_1()
@@ -1836,10 +1856,8 @@ impl RepoDetailView {
fn attach_store(&mut self, store: &Entity<RepoStore>, cx: &mut Context<Self>) { fn attach_store(&mut self, store: &Entity<RepoStore>, cx: &mut Context<Self>) {
self._subscriptions self._subscriptions
.push(cx.observe(store, |this, _store, cx| { .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); this.refresh_ready_statuses(cx);
cx.notify();
})); }));
self.refresh_ready_statuses(cx); self.refresh_ready_statuses(cx);
} }
@@ -1882,6 +1900,26 @@ impl RepoDetailView {
}); });
} }
/// The ready-to-push statuses of this repository in the global checkouts
/// store changed since they last drove a render.
///
/// Updates the cached slices. `None` store (a local, not yet published,
/// repository) has no statuses.
fn refresh_statuses(&mut self, cx: &mut Context<Self>) -> bool {
let Some(entity) = self.store.clone() else {
return false;
};
let addr = entity.read(cx).addr().clone();
let checkouts = CheckoutsStore::global(cx).read(cx);
let ready_statuses = checkouts.ready_statuses_of(&addr);
let push_statuses = checkouts.push_statuses_of(&addr);
let changed = ready_statuses != self.ready_statuses || push_statuses != self.push_statuses;
self.ready_statuses = ready_statuses;
self.push_statuses = push_statuses;
changed
}
/// The first checkout ready for a pull request on this repository. /// The first checkout ready for a pull request on this repository.
/// Not covered by an open PR of the signed-in user. /// Not covered by an open PR of the signed-in user.
/// Not dismissed in this panel. /// Not dismissed in this panel.
@@ -1895,7 +1933,7 @@ impl RepoDetailView {
return None; return None;
} }
let statuses = CheckoutsStore::global(cx).read(cx).statuses_of(&addr); let statuses = CheckoutsStore::global(cx).read(cx).ready_statuses_of(&addr);
'status: for status in statuses { 'status: for status in statuses {
if self if self
@@ -1918,15 +1956,19 @@ impl RepoDetailView {
} }
/// The first checkout of this owned repository with unpushed commits. /// The first checkout of this owned repository with unpushed commits.
///
/// Not dismissed in this panel. /// Not dismissed in this panel.
fn push_suggestion(&self, cx: &App) -> Option<CheckoutStatus> { fn push_suggestion(&self, cx: &App) -> Option<CheckoutStatus> {
let entity = self.store.as_ref()?; let entity = self.store.as_ref()?;
let user = Backend::global(cx).read(cx).current_user()?; let user = Backend::global(cx).read(cx).current_user()?;
if !entity.read(cx).is_author(&user) { if !entity.read(cx).is_author(&user) {
return None; return None;
} }
let addr = entity.read(cx).addr().clone(); let addr = entity.read(cx).addr().clone();
let statuses = CheckoutsStore::global(cx).read(cx).push_statuses_of(&addr); let statuses = CheckoutsStore::global(cx).read(cx).push_statuses_of(&addr);
statuses.into_iter().find(|status| { statuses.into_iter().find(|status| {
!self !self
.banner_dismissed .banner_dismissed
@@ -1935,52 +1977,60 @@ impl RepoDetailView {
} }
/// The ready-to-push banner of an owned repository. /// The ready-to-push banner of an owned repository.
///
/// A local checkout has unpushed commits, with a Push action and a dismiss control. /// A local checkout has unpushed commits, with a Push action and a dismiss control.
fn render_push_banner(&self, cx: &Context<Self>) -> Option<AnyElement> { fn render_push_banner(&self, cx: &Context<Self>) -> Option<AnyElement> {
let status = self.push_suggestion(cx)?; let status = self.push_suggestion(cx)?;
let key = (status.path.clone(), status.branch.clone());
let path = status.path.clone();
let commits = if status.ahead == 1 { let commits = if status.ahead == 1 {
"1 commit".to_owned() "1 commit".to_owned()
} else { } else {
format!("{} commits", status.ahead) format!("{} commits", status.ahead)
}; };
let message = SharedString::from(format!( let message = SharedString::from(format!(
"{} has {} ready to push in {}", "{} has {} ready to push in {}",
status.branch, status.branch,
commits, commits,
status.path.display() status.path.display()
)); ));
let key = (status.path.clone(), status.branch.clone());
let view = cx.entity().clone();
let path = status.path.clone();
let pushing = self.pushing;
Some( Some(
h_flex() h_flex()
.gap_2()
.px_4() .px_4()
.pt_1() .gap_2()
.w_full() .w_full()
.items_center() .items_center()
.justify_between()
.child(div().text_sm().child(message))
.child( .child(
Alert::info("repo-unpushed", message) h_flex()
.banner() .gap_1()
.flex_1() .child(
.on_close(move |_event, _window, cx| { Button::new("push-checkout-banner")
view.update(cx, |this, _| { .icon(IconName::ArrowUp)
this.banner_dismissed.insert(key.clone()); .label("Push")
}); .small()
}), .primary()
) .loading(self.pushing)
.child( .disabled(self.pushing)
Button::new("push-checkout-banner") .on_click(cx.listener(move |this, _event, window, cx| {
.small() this.push_unpushed_checkout(path.clone(), window, cx);
.icon(CustomIconName::Init) })),
.label("Push") )
.loading(pushing) .child(
.disabled(pushing) Button::new("close-repo")
.on_click(cx.listener(move |this, _event, window, cx| { .icon(IconName::Close)
this.push_unpushed_checkout(path.clone(), window, cx); .small()
})), .ghost()
.disabled(self.pushing)
.on_click(cx.listener(move |this, _ev, _window, cx| {
this.banner_dismissed.insert(key.clone());
cx.notify();
})),
),
) )
.into_any_element(), .into_any_element(),
) )
@@ -2259,8 +2309,10 @@ impl Render for RepoDetailView {
.image_cache(gpui::retain_all("repo")) .image_cache(gpui::retain_all("repo"))
.id("repo") .id("repo")
.size_full() .size_full()
.when_some(banner, |this, banner| {
this.child(v_flex().gap_1().py_4().bg(cx.theme().muted).child(banner))
})
.child(self.render_header(cx)) .child(self.render_header(cx))
.when_some(banner, |this, banner| this.child(banner))
.when_some(self.error.clone(), |this, error| { .when_some(self.error.clone(), |this, error| {
this.child( this.child(
Alert::error("repo-error", error) Alert::error("repo-error", error)
+197 -166
View File
@@ -1,6 +1,7 @@
use std::collections::HashSet; use std::collections::{HashMap, HashSet};
use std::ops::Range; use std::ops::Range;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use assets::CustomIconName; use assets::CustomIconName;
@@ -9,18 +10,19 @@ use dock::{
}; };
use gpui::prelude::*; use gpui::prelude::*;
use gpui::{ use gpui::{
AnyElement, App, Context, Div, Entity, EventEmitter, FocusHandle, Focusable, ObjectFit, Render, AnyElement, App, Context, Div, EventEmitter, FocusHandle, Focusable, ObjectFit, Render,
SharedString, Subscription, WeakEntity, Window, div, img, px, uniform_list, SharedString, Subscription, WeakEntity, Window, div, img, px, uniform_list,
}; };
use gpui_base::Button as BaseButton; use gpui_base::Button as BaseButton;
use gpui_component::badge::Badge;
use gpui_component::button::{Button, ButtonVariants}; use gpui_component::button::{Button, ButtonVariants};
use gpui_component::input::InputState; use gpui_component::input::InputState;
use gpui_component::{ActiveTheme, Icon, IconName, Sizable, StyledExt, h_flex, v_flex}; use gpui_component::{ActiveTheme, Icon, IconName, Sizable, StyledExt, h_flex, v_flex};
use signed_core::{Announcement, identifier_from_name}; use signed_core::{Announcement, RepoAddr, identifier_from_name};
use signed_state::{ use signed_state::{
Backend, BackendEvent, CheckoutsStore, LocalReposStore, Profile, ProfileStore, RepoListStore, 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}; use super::{RepoDetailView, RepoListView, open_repo_panel};
@@ -33,114 +35,176 @@ mod settings_dialog;
use self::onboarding_dialog::OnboardingState; use self::onboarding_dialog::OnboardingState;
/// Left-dock panel with navigation entries.
/// Entries open content panels in the dock area.
pub struct SidebarPanel { pub struct SidebarPanel {
focus_handle: FocusHandle, focus_handle: FocusHandle,
dock_area: WeakEntity<DockArea>, dock_area: WeakEntity<DockArea>,
explore: Option<WeakEntity<RepoListView>>, explore: Option<WeakEntity<RepoListView>>,
logged_in: bool, /// Artwork for the sign-in screen.
/// Repositories the current user announced, listed under the All Repositories heading.
/// Recreated when the signer changes.
my_repos: Option<Entity<RepoListStore>>,
/// Observes the current user's repo store so the list re-renders.
my_repos_subscription: Option<Subscription>,
/// Banner artwork behind the sign-in screen.
/// Picked at random from the bundled `backgrounds/` assets.
banner: SharedString, banner: SharedString,
/// Observes the local-repository scan so new discoveries re-render. /// The signed-in user's announced repositories, newest first.
_local_repos_subscription: Subscription, announcements: Arc<Vec<Announcement>>,
/// Observes the checkouts store. /// Local repositories found by the scan that are not announced yet.
/// Its ready-to-push statuses feed the badges on the user's repo rows. local_repos: Arc<Vec<PathBuf>>,
_checkouts_subscription: Subscription, /// A local scan is currently running.
_subscription: Subscription, scanning: bool,
/// Unpushed local commits per announced repository, the row badge counts.
unpushed: HashMap<RepoAddr, usize>,
_subscriptions: Vec<Subscription>,
} }
impl SidebarPanel { impl SidebarPanel {
pub fn new(dock_area: WeakEntity<DockArea>, cx: &mut Context<Self>) -> Self { 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 backend = Backend::global(cx);
let logged_in = backend.read(cx).current_user().is_some(); let repos = RepoListStore::global(cx);
let local = LocalReposStore::global(cx);
let checkouts = CheckoutsStore::global(cx);
let subscription = cx.subscribe(&backend, |this, backend, event, cx| { let mut subscriptions = Vec::new();
match event {
BackendEvent::SignerChanged => { // Identity changes swap the whole sidebar between the sign-in screen and the signed-in content.
this.logged_in = backend.read(cx).current_user().is_some(); subscriptions.push(cx.subscribe(&backend, |this, _backend, event, cx| {
this.refresh_my_repos(cx); let signer_changed = matches!(event, BackendEvent::SignerChanged);
} let signer_required = matches!(event, BackendEvent::SignerRequired);
BackendEvent::SignerRequired => {
this.logged_in = false; if !signer_changed && !signer_required {
this.banner = pick_banner(); return;
this.my_repos = None;
this.my_repos_subscription = None;
}
_ => return,
} }
cx.notify();
});
let local_repos_subscription = cx.observe(&local_repos_store, |_, _, cx| { if signer_required {
cx.notify(); this.banner = pick_banner();
}); }
let checkouts_store = CheckoutsStore::global(cx); if this.refresh(cx) || signer_required {
let checkouts_subscription = cx.observe(&checkouts_store, |_, _, cx| { cx.notify();
cx.notify(); }
}); }));
let mut panel = Self { // The merged list re-derives when announcements or the local scan change.
subscriptions.push(cx.observe(&repos, |this, _repos, cx| {
if this.refresh(cx) {
cx.notify();
}
}));
// The local scan re-derives when announcements or the local scan change.
subscriptions.push(cx.observe(&local, |this, _local, cx| {
if this.refresh(cx) {
cx.notify();
}
}));
// Push statuses are recomputed in the background; only the badge counts change.
subscriptions.push(cx.observe(&checkouts, |this, _checkouts, cx| {
if this.refresh_unpushed(cx) {
cx.notify();
}
}));
let mut this = Self {
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
dock_area, dock_area,
logged_in,
explore: None, explore: None,
my_repos: None,
my_repos_subscription: None,
banner: pick_banner(), banner: pick_banner(),
_local_repos_subscription: local_repos_subscription, announcements: Arc::new(Vec::new()),
_checkouts_subscription: checkouts_subscription, local_repos: Arc::new(Vec::new()),
_subscription: subscription, scanning: false,
unpushed: HashMap::new(),
_subscriptions: subscriptions,
}; };
if logged_in { // Seed the snapshot right away.
panel.refresh_my_repos(cx); // The stores may already hold data from before the panel opened.
} // The first render must not depend on a later store update.
this.refresh(cx);
panel this
} }
/// Recreate the store listing the current user's repositories. /// The sidebar renders only its own derived fields, never the stores
/// Watch each repository for unpushed local work. /// directly. Because the panel is a cached view, a store update alone does
fn refresh_my_repos(&mut self, cx: &mut Context<Self>) { /// not re-render it: the observers notify this panel, which re-runs
self.my_repos_subscription = None; /// `render` over the fresh snapshot.
///
/// Returns `true` when a rendered field changed.
fn refresh(&mut self, cx: &mut Context<Self>) -> bool {
let backend = Backend::global(cx); let backend = Backend::global(cx);
let author = backend.read(cx).current_user(); let user = 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() { let repo_list = RepoListStore::global(cx);
self.my_repos_subscription = Some(cx.observe(store, |_this, store, cx| { let announcements = user
cx.notify(); .as_ref()
// These are the signed-in user's own repositories. .map(|user| repo_list.read(cx).announcements_of(user))
// Request their ready-to-push statuses, deduplicated per repository. .unwrap_or_default();
// The rows carry a badge while local work is unpushed.
let addrs: Vec<_> = store // A scanned repository is dropped from the local list
.read(cx) // once the user announces it, so it is not listed twice.
.announcements let local = LocalReposStore::global(cx);
.iter() let scanning = local.read(cx).scanning;
.map(|a| a.addr())
.collect(); let local_repos = {
let checkouts = CheckoutsStore::global(cx); let ids: HashSet<String> = announcements.iter().map(|a| a.id.clone()).collect();
checkouts.update(cx, |checkouts, cx| { local
for addr in addrs { .read(cx)
checkouts.request_push_statuses(&addr, cx); .repos
} .iter()
}); .filter(|path| {
})); let Some(name) = path.file_name() else {
return true;
};
!ids.contains(&identifier_from_name(&name.to_string_lossy()))
})
.cloned()
.collect()
};
let announcements_changed = *self.announcements != announcements;
let local_changed = *self.local_repos != local_repos;
let scanning_changed = self.scanning != scanning;
self.announcements = Arc::new(announcements);
self.local_repos = Arc::new(local_repos);
self.scanning = scanning;
if announcements_changed {
self.request_push_watches(cx);
self.unpushed.clear();
} }
announcements_changed || local_changed || scanning_changed
}
/// Recompute the badge counts from the global checkouts store's ready-to-push statuses
fn refresh_unpushed(&mut self, cx: &mut Context<Self>) -> bool {
let checkouts = CheckoutsStore::global(cx).read(cx);
let mut unpushed = HashMap::with_capacity(self.announcements.len());
for announcement in self.announcements.iter() {
let addr = announcement.addr();
let count = checkouts.unpushed(&addr);
if count > 0 {
unpushed.insert(addr, count);
}
}
if unpushed == self.unpushed {
return false;
}
self.unpushed = unpushed;
true
}
/// Keep the `ready to push` statuses of the announced repositories current.
fn request_push_watches(&self, cx: &mut Context<Self>) {
let checkouts = CheckoutsStore::global(cx);
checkouts.update(cx, |checkouts, cx| {
for announcement in self.announcements.iter() {
checkouts.request_push_statuses(&announcement.addr(), cx);
}
});
} }
/// Open the Explore repository list panel in the dock area's center. /// Open the Explore repository list panel in the dock area's center.
/// No-op if it is already open.
pub fn open_explore(&mut self, window: &mut Window, cx: &mut Context<Self>) { pub fn open_explore(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self if self
.explore .explore
@@ -193,25 +257,23 @@ impl SidebarPanel {
} }
/// Open a local repository's detail view in the dock's center. /// Open a local repository's detail view in the dock's center.
///
/// 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, 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, window, cx));
let _ = self.dock_area.update(cx, |dock_area, cx| { self.dock_area
add_center_panel(dock_area, panel_handle(detail), window, cx); .update(cx, |dock_area, cx| {
}); add_center_panel(dock_area, panel_handle(detail), window, cx);
})
.ok();
} }
/// The All Repositories section of the sidebar. fn render_repos(&self, cx: &mut Context<Self>) -> impl IntoElement {
/// A header with the create button above the current user's repositories. let announcements = self.announcements.clone();
/// Rendered lazily through a [`uniform_list`]. let local_repos = self.local_repos.clone();
/// Followed by local git repositories from the startup scan. let scanning = self.scanning;
fn render_my_repos(&self, cx: &mut Context<Self>) -> impl IntoElement {
let store = self.my_repos.as_ref();
let local = LocalReposStore::global(cx);
let local_repos = local.read(cx).repos.clone();
let scanning = local.read(cx).scanning;
v_flex() v_flex()
.px_2() .px_2()
@@ -258,57 +320,36 @@ impl SidebarPanel {
), ),
), ),
) )
.when_some(store, |builder, store| { .map(|this| {
let announcements = store.read(cx).announcements.clone(); // Merged list, the user's NIP-34 repositories and local repositories discovered.
// 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 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()))
})
.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(); let total = announcements.len() + local_repos.len();
if total == 0 { if total == 0 {
builder.child( this.child(
div() div()
.flex_1() .flex_1()
.px_2() .px_2()
.py_1() .py_1()
.text_xs() .text_xs()
.text_color(cx.theme().muted_foreground) .text_color(cx.theme().muted_foreground)
.child(if scanning { .map(|this| {
"Scanning for local repositories…" if scanning {
} else { this.child("Scanning for local repositories…")
"No repositories yet" } else {
this.child("No repositories yet")
}
}), }),
) )
} else { } else {
builder.child( this.child(
uniform_list( uniform_list(
"repos", "repos",
total, total,
cx.processor(move |this, range: Range<usize>, _window, cx| { cx.processor(move |this, range: Range<usize>, _, cx| {
range range
.map(|ix| { .map(|ix| {
this.render_repo_row_at( this.render_repo_at(&announcements, &local_repos, ix, cx)
&announcements, .into_any_element()
&local_repos,
ix,
cx,
)
.into_any_element()
}) })
.collect() .collect()
}), }),
@@ -321,7 +362,7 @@ impl SidebarPanel {
} }
/// One row of the merged sidebar list, a NIP-34 or a local repository. /// One row of the merged sidebar list, a NIP-34 or a local repository.
fn render_repo_row_at( fn render_repo_at(
&self, &self,
announcements: &[Announcement], announcements: &[Announcement],
local_repos: &[PathBuf], local_repos: &[PathBuf],
@@ -345,27 +386,21 @@ impl SidebarPanel {
announcement: &Announcement, announcement: &Announcement,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> impl IntoElement { ) -> impl IntoElement {
let name = announcement let name = announcement.name().map(SharedString::from);
.name
.as_deref()
.map(SharedString::from)
.unwrap_or_else(|| SharedString::from(announcement.id.clone()));
let avatar = PixelAvatar::new(format!("{}:{}", announcement.owner, announcement.id)); 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. // Badge with the unpushed commit count of the repository's local checkouts.
// The commits are ready to push to the grasp servers. let unpushed = self
let unpushed: usize = CheckoutsStore::global(cx) .unpushed
.read(cx) .get(&announcement.addr())
.push_statuses_of(&announcement.addr()) .copied()
.iter() .unwrap_or(0);
.map(|status| status.ahead as usize)
.sum();
let announcement = announcement.clone(); let mut row = NavItem::new(format!("repo:{}", announcement.id), name, avatar);
let mut row = NavItem::new(format!("my-repo:{}", announcement.id), name, avatar);
if unpushed > 0 { if unpushed > 0 {
row = row.suffix(CountBadge::new(unpushed)); row = row.suffix(Badge::new().count(unpushed).xsmall());
} }
row.on_click( row.on_click(
@@ -373,29 +408,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. /// 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 { fn render_local_row(&self, path: &Path, cx: &mut Context<Self>) -> impl IntoElement {
let name = path let name = path
.file_name() .file_name()
.map(|name| name.to_string_lossy().into_owned()) .map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(|| path.display().to_string()); .unwrap_or("Untitled".into());
let path = path.to_path_buf(); let path = path.to_path_buf();
let avatar = PixelAvatar::new(path.to_string_lossy());
NavItem::new( NavItem::new(format!("local-repo:{}", path.display()), name, avatar)
format!("local-repo:{}", path.display()), .suffix(
name, Icon::new(IconName::TriangleAlert)
PixelAvatar::new(path.to_string_lossy()), .small()
) .text_color(cx.theme().warning),
.suffix( )
Icon::new(IconName::TriangleAlert) .on_click(cx.listener(move |this, _ev, window, cx| {
.small() this.open_local_repo(path.clone(), window, cx);
.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. /// Show the Import Identity dialog.
@@ -434,7 +466,6 @@ impl SidebarPanel {
} }
/// Sign-in placeholder shown while logged out. /// 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 { fn render_sign_in(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
v_flex() v_flex()
.size_full() .size_full()
@@ -540,10 +571,6 @@ impl Focusable for SidebarPanel {
impl Render for SidebarPanel { impl Render for SidebarPanel {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if !self.logged_in {
return self.render_sign_in(window, cx);
}
let backend = Backend::global(cx); let backend = Backend::global(cx);
let profile_store = ProfileStore::global(cx); let profile_store = ProfileStore::global(cx);
@@ -552,6 +579,10 @@ impl Render for SidebarPanel {
.current_user() .current_user()
.map(|public_key| profile_store.read(cx).get(&public_key)); .map(|public_key| profile_store.read(cx).get(&public_key));
if profile.is_none() {
return self.render_sign_in(window, cx);
}
v_flex() v_flex()
.size_full() .size_full()
.justify_between() .justify_between()
@@ -598,7 +629,7 @@ impl Render for SidebarPanel {
)), )),
), ),
) )
.child(self.render_my_repos(cx)), .child(self.render_repos(cx)),
) )
.child( .child(
v_flex() v_flex()
@@ -267,23 +267,14 @@ impl SettingsControls {
/// Open the Settings dialog. /// Open the Settings dialog.
pub fn open(window: &mut Window, cx: &mut App) { pub fn open(window: &mut Window, cx: &mut App) {
let controls = Rc::new(SettingsControls::new(window, cx)); 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| { window.open_dialog(cx, move |dialog, _window, cx| {
let dialog_state = dialog_state.clone(); let controls = controls.clone();
dialog dialog
.title("Settings") .title("Settings")
.width(px(650.)) .width(px(650.))
.h(px(560.)) .h(px(560.))
.child(settings_view(&dialog_state.0, cx)) .child(settings_view(&controls, cx))
}); });
} }