update
This commit is contained in:
@@ -24,6 +24,7 @@ use utils::relative_time;
|
|||||||
pub(super) mod detail;
|
pub(super) mod detail;
|
||||||
|
|
||||||
use self::detail::IssueDetailView;
|
use self::detail::IssueDetailView;
|
||||||
|
use super::status_list::{StatusCounts, filter_by_status};
|
||||||
|
|
||||||
const ISSUE_ROW_HEIGHT: f32 = 73.;
|
const ISSUE_ROW_HEIGHT: f32 = 73.;
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ pub struct IssuesView {
|
|||||||
filter: IssueFilter,
|
filter: IssueFilter,
|
||||||
item_sizes: Rc<Vec<Size<Pixels>>>,
|
item_sizes: Rc<Vec<Size<Pixels>>>,
|
||||||
visible_issues: Vec<usize>,
|
visible_issues: Vec<usize>,
|
||||||
counts: (usize, usize, usize),
|
counts: StatusCounts,
|
||||||
// A filter change notifies even when the visible rows are unchanged,
|
// A filter change notifies even when the visible rows are unchanged,
|
||||||
// e.g. switching between two empty filters.
|
// e.g. switching between two empty filters.
|
||||||
synced_filter: IssueFilter,
|
synced_filter: IssueFilter,
|
||||||
@@ -85,7 +86,7 @@ impl IssuesView {
|
|||||||
filter: IssueFilter::Open,
|
filter: IssueFilter::Open,
|
||||||
item_sizes: Rc::new(Vec::new()),
|
item_sizes: Rc::new(Vec::new()),
|
||||||
visible_issues: Vec::new(),
|
visible_issues: Vec::new(),
|
||||||
counts: (0, 0, 0),
|
counts: StatusCounts::default(),
|
||||||
synced_filter: IssueFilter::Open,
|
synced_filter: IssueFilter::Open,
|
||||||
scroll_handle: VirtualListScrollHandle::new(),
|
scroll_handle: VirtualListScrollHandle::new(),
|
||||||
_subscription: subscription,
|
_subscription: subscription,
|
||||||
@@ -97,25 +98,11 @@ impl IssuesView {
|
|||||||
|
|
||||||
let (visible_issues, counts) = {
|
let (visible_issues, counts) = {
|
||||||
let store = self.store.read(cx);
|
let store = self.store.read(cx);
|
||||||
let mut counts = (0usize, 0usize, 0usize);
|
filter_by_status(
|
||||||
|
&store.issues,
|
||||||
let visible_issues: Vec<usize> = store
|
|issue| store.status_of(issue),
|
||||||
.issues
|
|status| filter.matches(status),
|
||||||
.iter()
|
)
|
||||||
.enumerate()
|
|
||||||
.filter_map(|(ix, issue)| {
|
|
||||||
let status = store.status_of(issue);
|
|
||||||
counts.0 += 1;
|
|
||||||
match status {
|
|
||||||
RepoStatus::Open => counts.1 += 1,
|
|
||||||
RepoStatus::Closed => counts.2 += 1,
|
|
||||||
RepoStatus::Draft | RepoStatus::Applied => {}
|
|
||||||
}
|
|
||||||
filter.matches(status).then_some(ix)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
(visible_issues, counts)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let filter_changed = self.synced_filter != filter;
|
let filter_changed = self.synced_filter != filter;
|
||||||
@@ -218,7 +205,7 @@ impl IssuesView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let (total, open, closed) = self.counts;
|
let counts = self.counts;
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.px_4()
|
.px_4()
|
||||||
@@ -234,7 +221,7 @@ impl IssuesView {
|
|||||||
.child(
|
.child(
|
||||||
SegmentButton::new("all", "All")
|
SegmentButton::new("all", "All")
|
||||||
.icon(Icon::new(CustomIconName::GitIssueDone))
|
.icon(Icon::new(CustomIconName::GitIssueDone))
|
||||||
.count(total)
|
.count(counts.total)
|
||||||
.selected(self.filter == IssueFilter::All)
|
.selected(self.filter == IssueFilter::All)
|
||||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||||
this.filter = IssueFilter::All;
|
this.filter = IssueFilter::All;
|
||||||
@@ -244,7 +231,7 @@ impl IssuesView {
|
|||||||
.child(
|
.child(
|
||||||
SegmentButton::new("open", "Open")
|
SegmentButton::new("open", "Open")
|
||||||
.icon(Icon::new(CustomIconName::GitIssueOpen))
|
.icon(Icon::new(CustomIconName::GitIssueOpen))
|
||||||
.count(open)
|
.count(counts.open)
|
||||||
.selected(self.filter == IssueFilter::Open)
|
.selected(self.filter == IssueFilter::Open)
|
||||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||||
this.filter = IssueFilter::Open;
|
this.filter = IssueFilter::Open;
|
||||||
@@ -254,7 +241,7 @@ impl IssuesView {
|
|||||||
.child(
|
.child(
|
||||||
SegmentButton::new("closed", "Closed")
|
SegmentButton::new("closed", "Closed")
|
||||||
.icon(Icon::new(CustomIconName::GitIssueClosed))
|
.icon(Icon::new(CustomIconName::GitIssueClosed))
|
||||||
.count(closed)
|
.count(counts.closed)
|
||||||
.selected(self.filter == IssueFilter::Closed)
|
.selected(self.filter == IssueFilter::Closed)
|
||||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||||
this.filter = IssueFilter::Closed;
|
this.filter = IssueFilter::Closed;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ mod repo;
|
|||||||
mod repo_list;
|
mod repo_list;
|
||||||
mod send_patch;
|
mod send_patch;
|
||||||
pub(crate) mod sidebar;
|
pub(crate) mod sidebar;
|
||||||
|
mod status_list;
|
||||||
pub(crate) mod tree;
|
pub(crate) mod tree;
|
||||||
|
|
||||||
pub use inbox::InboxView;
|
pub use inbox::InboxView;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ pub(super) mod new;
|
|||||||
use self::detail::PullRequestDetailView;
|
use self::detail::PullRequestDetailView;
|
||||||
use self::new::open_new_pull_panel;
|
use self::new::open_new_pull_panel;
|
||||||
use super::send_patch::open_send_patch_panel;
|
use super::send_patch::open_send_patch_panel;
|
||||||
|
use super::status_list::{StatusCounts, filter_by_status};
|
||||||
use crate::views::repo::RepoAction;
|
use crate::views::repo::RepoAction;
|
||||||
|
|
||||||
const ROW_HEIGHT: f32 = 73.;
|
const ROW_HEIGHT: f32 = 73.;
|
||||||
@@ -59,8 +60,7 @@ pub struct PullRequestsView {
|
|||||||
item_sizes: Rc<Vec<Size<Pixels>>>,
|
item_sizes: Rc<Vec<Size<Pixels>>>,
|
||||||
/// Indices into the store's `pull_requests` matching [`Self::filter`].
|
/// Indices into the store's `pull_requests` matching [`Self::filter`].
|
||||||
visible_prs: Vec<usize>,
|
visible_prs: Vec<usize>,
|
||||||
/// Header counts `(total, open, closed, draft, merged)`.
|
counts: StatusCounts,
|
||||||
counts: (usize, usize, usize, usize, usize),
|
|
||||||
// A filter change notifies even when the visible rows are unchanged,
|
// A filter change notifies even when the visible rows are unchanged,
|
||||||
// e.g. switching between two empty filters.
|
// e.g. switching between two empty filters.
|
||||||
synced_filter: PullRequestFilter,
|
synced_filter: PullRequestFilter,
|
||||||
@@ -93,7 +93,7 @@ impl PullRequestsView {
|
|||||||
filter: PullRequestFilter::Open,
|
filter: PullRequestFilter::Open,
|
||||||
item_sizes: Rc::new(Vec::new()),
|
item_sizes: Rc::new(Vec::new()),
|
||||||
visible_prs: Vec::new(),
|
visible_prs: Vec::new(),
|
||||||
counts: (0, 0, 0, 0, 0),
|
counts: StatusCounts::default(),
|
||||||
synced_filter: PullRequestFilter::Open,
|
synced_filter: PullRequestFilter::Open,
|
||||||
scroll_handle: VirtualListScrollHandle::new(),
|
scroll_handle: VirtualListScrollHandle::new(),
|
||||||
_subscription: subscription,
|
_subscription: subscription,
|
||||||
@@ -105,32 +105,15 @@ impl PullRequestsView {
|
|||||||
|
|
||||||
let (visible_prs, counts) = {
|
let (visible_prs, counts) = {
|
||||||
let store = self.store.read(cx);
|
let store = self.store.read(cx);
|
||||||
let mut counts = (0usize, 0usize, 0usize, 0usize, 0usize);
|
let roots = store
|
||||||
|
|
||||||
let visible_prs: Vec<usize> = store
|
|
||||||
.pull_requests
|
.pull_requests
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.filter(|pr| pr.kind == Kind::GitPullRequest);
|
||||||
.filter_map(|(ix, pr)| {
|
filter_by_status(
|
||||||
if pr.kind != Kind::GitPullRequest {
|
roots,
|
||||||
return None;
|
|pr| store.status_of(pr),
|
||||||
}
|
|status| filter.matches(status),
|
||||||
|
)
|
||||||
let status = store.status_of(pr);
|
|
||||||
counts.0 += 1;
|
|
||||||
|
|
||||||
match status {
|
|
||||||
RepoStatus::Open => counts.1 += 1,
|
|
||||||
RepoStatus::Closed => counts.2 += 1,
|
|
||||||
RepoStatus::Draft => counts.3 += 1,
|
|
||||||
RepoStatus::Applied => counts.4 += 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
filter.matches(status).then_some(ix)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
(visible_prs, counts)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let filter_changed = self.synced_filter != filter;
|
let filter_changed = self.synced_filter != filter;
|
||||||
@@ -236,7 +219,7 @@ impl PullRequestsView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let (total, open, closed, draft, merged) = self.counts;
|
let counts = self.counts;
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.px_4()
|
.px_4()
|
||||||
@@ -252,7 +235,7 @@ impl PullRequestsView {
|
|||||||
.child(
|
.child(
|
||||||
SegmentButton::new("all", "All")
|
SegmentButton::new("all", "All")
|
||||||
.icon(Icon::new(CustomIconName::GitPullRequest))
|
.icon(Icon::new(CustomIconName::GitPullRequest))
|
||||||
.count(total)
|
.count(counts.total)
|
||||||
.selected(self.filter == PullRequestFilter::All)
|
.selected(self.filter == PullRequestFilter::All)
|
||||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||||
this.filter = PullRequestFilter::All;
|
this.filter = PullRequestFilter::All;
|
||||||
@@ -262,7 +245,7 @@ impl PullRequestsView {
|
|||||||
.child(
|
.child(
|
||||||
SegmentButton::new("open", "Open")
|
SegmentButton::new("open", "Open")
|
||||||
.icon(Icon::new(CustomIconName::GitPullRequest))
|
.icon(Icon::new(CustomIconName::GitPullRequest))
|
||||||
.count(open)
|
.count(counts.open)
|
||||||
.selected(self.filter == PullRequestFilter::Open)
|
.selected(self.filter == PullRequestFilter::Open)
|
||||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||||
this.filter = PullRequestFilter::Open;
|
this.filter = PullRequestFilter::Open;
|
||||||
@@ -272,7 +255,7 @@ impl PullRequestsView {
|
|||||||
.child(
|
.child(
|
||||||
SegmentButton::new("closed", "Closed")
|
SegmentButton::new("closed", "Closed")
|
||||||
.icon(Icon::new(CustomIconName::GitPullRequestClosed))
|
.icon(Icon::new(CustomIconName::GitPullRequestClosed))
|
||||||
.count(closed)
|
.count(counts.closed)
|
||||||
.selected(self.filter == PullRequestFilter::Closed)
|
.selected(self.filter == PullRequestFilter::Closed)
|
||||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||||
this.filter = PullRequestFilter::Closed;
|
this.filter = PullRequestFilter::Closed;
|
||||||
@@ -282,7 +265,7 @@ impl PullRequestsView {
|
|||||||
.child(
|
.child(
|
||||||
SegmentButton::new("draft", "Draft")
|
SegmentButton::new("draft", "Draft")
|
||||||
.icon(Icon::new(CustomIconName::GitPullRequestDraft))
|
.icon(Icon::new(CustomIconName::GitPullRequestDraft))
|
||||||
.count(draft)
|
.count(counts.draft)
|
||||||
.selected(self.filter == PullRequestFilter::Draft)
|
.selected(self.filter == PullRequestFilter::Draft)
|
||||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||||
this.filter = PullRequestFilter::Draft;
|
this.filter = PullRequestFilter::Draft;
|
||||||
@@ -292,7 +275,7 @@ impl PullRequestsView {
|
|||||||
.child(
|
.child(
|
||||||
SegmentButton::new("merged", "Merged")
|
SegmentButton::new("merged", "Merged")
|
||||||
.icon(Icon::new(CustomIconName::GitPullRequestMerged))
|
.icon(Icon::new(CustomIconName::GitPullRequestMerged))
|
||||||
.count(merged)
|
.count(counts.applied)
|
||||||
.selected(self.filter == PullRequestFilter::Merged)
|
.selected(self.filter == PullRequestFilter::Merged)
|
||||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||||
this.filter = PullRequestFilter::Merged;
|
this.filter = PullRequestFilter::Merged;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use nostr::prelude::*;
|
|||||||
use settings::{DEFAULT_GRASP_SERVERS, GraspServersSettings};
|
use settings::{DEFAULT_GRASP_SERVERS, GraspServersSettings};
|
||||||
use signed_state::Backend;
|
use signed_state::Backend;
|
||||||
|
|
||||||
|
use super::{normalize_server, server_host};
|
||||||
|
|
||||||
/// State of the grasp-server section of a publish dialog, so async results can be rendered.
|
/// State of the grasp-server section of a publish dialog, so async results can be rendered.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct GraspServersState {
|
pub struct GraspServersState {
|
||||||
@@ -155,7 +157,7 @@ fn render_server_row(
|
|||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.rounded(cx.theme().radius)
|
.rounded(cx.theme().radius)
|
||||||
.child(display_server(relay)),
|
.child(server_host(relay)),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new(format!("remove-relay:{ix}"))
|
Button::new(format!("remove-relay:{ix}"))
|
||||||
@@ -174,14 +176,6 @@ fn render_server_row(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows only the host, since grasp servers are entered without a scheme.
|
|
||||||
fn display_server(relay: &RelayUrl) -> SharedString {
|
|
||||||
relay
|
|
||||||
.domain()
|
|
||||||
.map(SharedString::from)
|
|
||||||
.unwrap_or_else(|| SharedString::from(relay.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Accepts a bare host as well as a full URL.
|
/// Accepts a bare host as well as a full URL.
|
||||||
fn add_relay(
|
fn add_relay(
|
||||||
state: &Entity<GraspServersState>,
|
state: &Entity<GraspServersState>,
|
||||||
@@ -194,14 +188,8 @@ fn add_relay(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let normalized = if value.contains("://") {
|
match normalize_server(&value) {
|
||||||
value.clone()
|
Some((_, relay)) => {
|
||||||
} else {
|
|
||||||
format!("wss://{value}")
|
|
||||||
};
|
|
||||||
|
|
||||||
match RelayUrl::parse(&normalized) {
|
|
||||||
Ok(relay) => {
|
|
||||||
state.update(cx, |state, _| {
|
state.update(cx, |state, _| {
|
||||||
state.error = None;
|
state.error = None;
|
||||||
if !state.grasp_servers.contains(&relay) {
|
if !state.grasp_servers.contains(&relay) {
|
||||||
@@ -210,7 +198,7 @@ fn add_relay(
|
|||||||
});
|
});
|
||||||
input.update(cx, |input, cx| input.set_value("", window, cx));
|
input.update(cx, |input, cx| input.set_value("", window, cx));
|
||||||
}
|
}
|
||||||
Err(_) => {
|
None => {
|
||||||
state.update(cx, |state, _| {
|
state.update(cx, |state, _| {
|
||||||
state.error = Some(format!("Invalid grasp server URL: {value}").into());
|
state.error = Some(format!("Invalid grasp server URL: {value}").into());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use gpui_base::Button as BaseButton;
|
|||||||
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 nostr::prelude::RelayUrl;
|
||||||
use signed_core::{Announcement, RepoAddr, 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,
|
||||||
@@ -542,6 +543,26 @@ impl SidebarPanel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Normalize a user-typed grasp server, adding a `wss://` scheme when none is given.
|
||||||
|
///
|
||||||
|
/// Returns the text to store and the parsed relay URL, or `None` when it is not a valid relay URL.
|
||||||
|
pub(super) fn normalize_server(input: &str) -> Option<(String, RelayUrl)> {
|
||||||
|
let text = if input.contains("://") {
|
||||||
|
input.to_owned()
|
||||||
|
} else {
|
||||||
|
format!("wss://{input}")
|
||||||
|
};
|
||||||
|
RelayUrl::parse(&text).ok().map(|relay| (text, relay))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The host of a relay URL, which is what the server lists show; the scheme is implied.
|
||||||
|
pub(super) fn server_host(relay: &RelayUrl) -> SharedString {
|
||||||
|
relay
|
||||||
|
.domain()
|
||||||
|
.map(SharedString::from)
|
||||||
|
.unwrap_or_else(|| SharedString::from(relay.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
fn pick_banner() -> SharedString {
|
fn pick_banner() -> SharedString {
|
||||||
let num = SystemTime::now()
|
let num = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
|
|||||||
@@ -18,10 +18,11 @@ use gpui_component::{
|
|||||||
ActiveTheme, IconName, IndexPath, Sizable, Theme, ThemeMode, ThemeRegistry, WindowExt, h_flex,
|
ActiveTheme, IconName, IndexPath, Sizable, Theme, ThemeMode, ThemeRegistry, WindowExt, h_flex,
|
||||||
v_flex,
|
v_flex,
|
||||||
};
|
};
|
||||||
use nostr::prelude::RelayUrl;
|
|
||||||
use settings::{AppearanceMode, Settings, SettingsStore};
|
use settings::{AppearanceMode, Settings, SettingsStore};
|
||||||
use signed_ui::{SelectOption, setting_block, setting_row};
|
use signed_ui::{SelectOption, setting_block, setting_row};
|
||||||
|
|
||||||
|
use super::{normalize_server, server_host};
|
||||||
|
|
||||||
/// Looks up the option index used to seed a [`SelectState`].
|
/// Looks up the option index used to seed a [`SelectState`].
|
||||||
fn selected_index(options: &[SelectOption], value: &str) -> Option<IndexPath> {
|
fn selected_index(options: &[SelectOption], value: &str) -> Option<IndexPath> {
|
||||||
options
|
options
|
||||||
@@ -454,13 +455,11 @@ fn grasp_server_editor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Shows only the host, since grasp servers are entered without a scheme.
|
/// Shows only the host, since grasp servers are entered without a scheme.
|
||||||
/// Matches how the publish dialogs display servers.
|
|
||||||
fn display_server(server: &str) -> SharedString {
|
fn display_server(server: &str) -> SharedString {
|
||||||
RelayUrl::parse(server)
|
match normalize_server(server) {
|
||||||
.ok()
|
Some((_, relay)) => server_host(&relay),
|
||||||
.and_then(|relay| relay.domain().map(|domain| domain.to_owned()))
|
None => SharedString::from(server.to_owned()),
|
||||||
.map(SharedString::from)
|
}
|
||||||
.unwrap_or_else(|| SharedString::from(server.to_owned()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn repositories_section(
|
fn repositories_section(
|
||||||
@@ -568,14 +567,9 @@ fn add_server(input: &Entity<InputState>, window: &mut Window, cx: &mut App) {
|
|||||||
if value.is_empty() {
|
if value.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let normalized = if value.contains("://") {
|
let Some((normalized, _)) = normalize_server(&value) else {
|
||||||
value
|
|
||||||
} else {
|
|
||||||
format!("wss://{value}")
|
|
||||||
};
|
|
||||||
if RelayUrl::parse(&normalized).is_err() {
|
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
|
|
||||||
let store = SettingsStore::global(cx);
|
let store = SettingsStore::global(cx);
|
||||||
store.update(cx, |store, cx| {
|
store.update(cx, |store, cx| {
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
use nostr::prelude::Event;
|
||||||
|
use signed_core::RepoStatus;
|
||||||
|
|
||||||
|
/// Root events counted by their resolved status.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub(crate) struct StatusCounts {
|
||||||
|
pub(crate) total: usize,
|
||||||
|
pub(crate) open: usize,
|
||||||
|
pub(crate) closed: usize,
|
||||||
|
pub(crate) draft: usize,
|
||||||
|
pub(crate) applied: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatusCounts {
|
||||||
|
fn record(&mut self, status: RepoStatus) {
|
||||||
|
self.total += 1;
|
||||||
|
match status {
|
||||||
|
RepoStatus::Open => self.open += 1,
|
||||||
|
RepoStatus::Closed => self.closed += 1,
|
||||||
|
RepoStatus::Draft => self.draft += 1,
|
||||||
|
RepoStatus::Applied => self.applied += 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Indices of `roots` whose status `keep` accepts, counting every root's status.
|
||||||
|
pub(crate) fn filter_by_status<'a>(
|
||||||
|
roots: impl IntoIterator<Item = &'a Event>,
|
||||||
|
status_of: impl Fn(&Event) -> RepoStatus,
|
||||||
|
keep: impl Fn(RepoStatus) -> bool,
|
||||||
|
) -> (Vec<usize>, StatusCounts) {
|
||||||
|
let mut counts = StatusCounts::default();
|
||||||
|
|
||||||
|
let visible = roots
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(index, root)| {
|
||||||
|
let status = status_of(root);
|
||||||
|
counts.record(status);
|
||||||
|
keep(status).then_some(index)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
(visible, counts)
|
||||||
|
}
|
||||||
@@ -199,30 +199,44 @@ Verify the duplication before extracting; each could be intentional.
|
|||||||
Files: `crates/workspace/src/views/pull_requests/mod.rs`,
|
Files: `crates/workspace/src/views/pull_requests/mod.rs`,
|
||||||
`crates/workspace/src/views/issues/mod.rs`
|
`crates/workspace/src/views/issues/mod.rs`
|
||||||
|
|
||||||
- [ ] Confirm the filter enum, visible-index rebuild, counts tuple, and
|
- [x] Confirm the shape. The two `rebuild`s are the same mechanic: one pass over a
|
||||||
virtual-list resize are the same shape.
|
root list, per-status counts, keep matching indices, early-return when
|
||||||
- [ ] If so, extract one small helper for the filtered index + counts + notify
|
filter/indices/counts are unchanged, resize the item sizes, notify.
|
||||||
decision and use it in both.
|
- [x] Extract `crates/workspace/src/views/status_list.rs` with `StatusCounts` and
|
||||||
|
`filter_by_status`. Both views now use it; the tuple counts were replaced by
|
||||||
|
`StatusCounts`. The notify decision stays local because it would need a trait
|
||||||
|
over the two filter enums.
|
||||||
|
|
||||||
### 3.2 Relay URL normalize/display
|
### 3.2 Relay URL normalize/display
|
||||||
|
|
||||||
Files: `crates/workspace/src/views/sidebar/settings_dialog.rs`,
|
Files: `crates/workspace/src/views/sidebar/settings_dialog.rs`,
|
||||||
`crates/workspace/src/views/sidebar/grasp_servers.rs`
|
`crates/workspace/src/views/sidebar/grasp_servers.rs`
|
||||||
|
|
||||||
- [ ] Confirm both pairs do prepend-scheme, parse, dedupe, and host-display.
|
- [x] Confirm both pairs do prepend-scheme, parse, dedupe, and host-display.
|
||||||
- [ ] Extract one normalize helper and one display helper. Decide the crate
|
- [x] Extract `normalize_server` and `server_host` into `sidebar/mod.rs`. They live
|
||||||
(check whether `signed_ui` may depend on `nostr`).
|
in `workspace`, not `signed_ui`: `signed_ui` does not depend on `nostr`, and
|
||||||
|
these are used only by the two sidebar modules. Dedupe differs per caller
|
||||||
|
(`Vec<RelayUrl>` vs persisted `Vec<String>`) and stays at the call site.
|
||||||
|
|
||||||
### 3.3 `crates/dock` vs the pinned `gpui_component` dock renderer
|
### 3.3 `crates/dock` vs the pinned `gpui_component` dock renderer
|
||||||
|
|
||||||
Files: `crates/dock/src/*` vs the pinned rev's `crates/ui/src/dock/*`
|
Files: `crates/dock/src/*` vs the pinned rev's `crates/ui/src/dock/*`
|
||||||
|
|
||||||
- [ ] Spike only: pick one part (`SignedTabGroupSkin` or `SignedTilesSkin`) and
|
- [x] Spike: `SignedTabGroupSkin` cannot delegate to the pinned upstream skin.
|
||||||
determine whether it can delegate to the upstream `DockSkin` trait
|
- `TabGroupSkin`, `TilesSkin` and `SkinShared` are `pub(crate)` in
|
||||||
implementation and keep only the Signed deltas (window controls in the tab
|
`gpui_component::ui`; only the opaque `DockSkin` renderer is public, and it
|
||||||
bar, plain-sidebar detection, prev/next, i18n).
|
holds that private shared state.
|
||||||
- [ ] Report effort and risk before doing any replacement. Do not start a
|
- `TabGroupRenderer`/`TilesRenderer` are all-or-nothing per method. The
|
||||||
rewrite of this crate in this cleanup.
|
Signed deltas (window controls, prev/next, plain-sidebar detection, i18n)
|
||||||
|
live *inside* `render_tab_bar` and `frame`. There is no hook below the whole
|
||||||
|
method, so "delegate and keep only the deltas" has no seam to hang on.
|
||||||
|
- Composing `Rc<DockSkin>` would still leave `render_tab_bar` a near-full
|
||||||
|
reimplementation while adding a dependency on upstream internals, for no
|
||||||
|
line reduction.
|
||||||
|
- [x] Effort/risk: high effort, high churn, no achievable reduction on this rev.
|
||||||
|
`SignedTilesSkin` is the same shape. Recommend keeping the fork as-is. A
|
||||||
|
future upstream change (public `DockSkin` with per-part hooks) would be the
|
||||||
|
precondition for any delegation.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user