diff --git a/crates/signed_state/src/backend.rs b/crates/signed_state/src/backend.rs index 668063a..d288578 100644 --- a/crates/signed_state/src/backend.rs +++ b/crates/signed_state/src/backend.rs @@ -60,6 +60,7 @@ impl BackendEvent { pub struct Backend { inner: NostrBackend, current_user: Option, + connected: bool, tasks: Vec>>, } @@ -106,6 +107,7 @@ impl Backend { let mut this = Self { inner, current_user: None, + connected: false, tasks: vec![pump], }; @@ -132,7 +134,11 @@ impl Backend { self.tasks.push(cx.spawn(async move |this, cx| { match task.await { Ok(()) => { - this.update(cx, |_this, cx| cx.emit(BackendEvent::Connected))?; + this.update(cx, |this, cx| { + this.connected = true; + cx.emit(BackendEvent::Connected); + cx.notify(); + })?; } Err(e) => { this.update(cx, |_this, cx| cx.emit(BackendEvent::error(e.to_string())))?; @@ -371,6 +377,11 @@ impl Backend { self.current_user } + /// Whether the relay bootstrap has completed. + pub fn is_connected(&self) -> bool { + self.connected + } + /// Update the signer (any type implementing the async signer traits, /// e.g. `Keys`, `NostrConnect`, a browser extension proxy). pub fn set_signer(&mut self, new_signer: T, cx: &mut Context) @@ -418,7 +429,11 @@ impl Backend { self.tasks.push(cx.spawn(async move |this, cx| { match task.await { Ok(()) => { - this.update(cx, |_this, cx| cx.emit(BackendEvent::Connected))?; + this.update(cx, |this, cx| { + this.connected = true; + cx.emit(BackendEvent::Connected); + cx.notify(); + })?; } Err(e) => { this.update(cx, |_this, cx| cx.emit(BackendEvent::error(e.to_string())))?; diff --git a/crates/signed_state/src/repo_list.rs b/crates/signed_state/src/repo_list.rs index 7d88dfd..379e678 100644 --- a/crates/signed_state/src/repo_list.rs +++ b/crates/signed_state/src/repo_list.rs @@ -20,7 +20,8 @@ pub struct RepoListStore { impl RepoListStore { /// Create a store. If `author` is `None`, all announcements are listed. pub fn new(author: Option, cx: &mut Context) -> Self { - let subscription = cx.subscribe(&Backend::global(cx), |this, _backend, event, cx| { + let backend = Backend::global(cx); + let subscription = cx.subscribe(&backend, |this, _backend, event, cx| { let relevant = match event { BackendEvent::NostrUpdate(update) => { update.kind == Kind::GitRepoAnnouncement @@ -60,9 +61,10 @@ impl RepoListStore { } fn subscribe_remote(&mut self, cx: &mut Context) { + let backend = Backend::global(cx); let author = self.author; - Backend::global(cx).update(cx, |backend, cx| { + backend.update(cx, |backend, cx| { let filter = match author { Some(a) => filters::announcements_by(a), None => filters::all_announcements(500), diff --git a/crates/workspace/src/views/repo_list.rs b/crates/workspace/src/views/repo_list.rs index db5636b..a51b2e0 100644 --- a/crates/workspace/src/views/repo_list.rs +++ b/crates/workspace/src/views/repo_list.rs @@ -1,11 +1,13 @@ use gpui::prelude::*; -use gpui::{Context, Entity, Render, SharedString, Subscription, Window, div, px}; -use gpui_component::scroll::ScrollableElement; +use gpui::{ + AnyElement, App, Context, Entity, Render, SharedString, Subscription, Window, div, px, + uniform_list, +}; use gpui_component::{ActiveTheme, StyledExt}; use signed_core::Announcement; use signed_state::{ProfileStore, RepoListStore}; -/// Browse all announced repositories. +/// Browse all announced repositories (works anonymously). pub struct RepoListView { store: Entity, _subscription: Subscription, @@ -21,44 +23,58 @@ impl RepoListView { _subscription: subscription, } } +} - fn render_card(&self, announcement: &Announcement, cx: &mut Context) -> impl IntoElement { - let name = announcement - .name - .clone() - .unwrap_or_else(|| announcement.id.clone()); +fn render_card(announcement: &Announcement, cx: &mut App) -> AnyElement { + let name = announcement + .name + .clone() + .unwrap_or_else(|| announcement.id.clone()); - let owner = ProfileStore::global(cx) - .update(cx, |store, cx| store.get(announcement.owner, cx)) - .name(); + let owner = ProfileStore::global(cx) + .update(cx, |store, cx| store.get(announcement.owner, cx)) + .name(); - div() - .v_flex() - .gap_1() - .px_4() - .py_3() - .border_b(px(1.)) - .border_color(cx.theme().border) - .child( - div() - .h_flex() - .gap_2() - .items_center() - .child(div().text_sm().font_semibold().child(name)) - .child( - div() - .text_xs() - .text_color(cx.theme().muted_foreground) - .child(owner), - ), - ) - .children(announcement.description.as_ref().map(|description| { - div() - .text_xs() - .text_color(cx.theme().muted_foreground) - .child(description.clone()) - })) - } + let description = announcement.description.clone().unwrap_or_default(); + + div() + .v_flex() + .h(px(60.)) + .justify_center() + .gap_1() + .px_4() + .border_b(px(1.)) + .border_color(cx.theme().border) + .child( + div() + .h_flex() + .gap_2() + .items_center() + .child( + div() + .text_sm() + .font_semibold() + .whitespace_nowrap() + .text_ellipsis() + .child(name), + ) + .child( + div() + .text_xs() + .text_color(cx.theme().muted_foreground) + .whitespace_nowrap() + .child(owner), + ), + ) + .child( + div() + .text_xs() + .text_color(cx.theme().muted_foreground) + .whitespace_nowrap() + .text_ellipsis() + .child(description), + ) + .into_any_element() } impl Render for RepoListView { @@ -66,22 +82,28 @@ impl Render for RepoListView { let announcements = self.store.read(cx).announcements.clone(); let count = announcements.len(); - let mut list = div().v_flex().flex_1().overflow_y_scrollbar(); - - if announcements.is_empty() { - list = list.child( - div().size_full().items_center().justify_center().child( + let body = if announcements.is_empty() { + div() + .size_full() + .v_flex() + .items_center() + .justify_center() + .child( div() .text_sm() .text_color(cx.theme().muted_foreground) .child("No repositories found. Waiting for relays..."), - ), - ); + ) + .into_any_element() } else { - for announcement in &announcements { - list = list.child(self.render_card(announcement, cx)); - } - } + uniform_list("repo-list", count, move |range, _window, cx| { + range + .map(|index| render_card(&announcements[index], cx)) + .collect() + }) + .size_full() + .into_any_element() + }; div() .v_flex() @@ -100,6 +122,6 @@ impl Render for RepoListView { .child(SharedString::from(format!(" ({count})"))), ), ) - .child(list) + .child(body) } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 47b6ee7..e0f9d80 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -14,9 +14,12 @@ pub struct Workspace { impl Workspace { pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let backend = Backend::global(cx); let repo_list = cx.new(|cx| RepoListView::new(window, cx)); - let subscription = cx.subscribe(&Backend::global(cx), |this, _backend, event, cx| { + let connected = backend.read(cx).is_connected(); + + let subscription = cx.subscribe(&backend, |this, _backend, event, cx| { match event { BackendEvent::Connected => this.status = "Connected".into(), BackendEvent::Error(error) => this.status = error.clone().into(), @@ -27,7 +30,11 @@ impl Workspace { Self { active_screen: repo_list.into(), - status: SharedString::from("Connecting..."), + status: if connected { + "Connected".into() + } else { + "Connecting...".into() + }, _subscription: subscription, } }