feat: add group discovery and group preview #59
@@ -4,8 +4,9 @@ use std::rc::Rc;
|
|||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
IntoElement, ParentElement, Render, ScrollStrategy, SharedString, Styled, Subscription, Task,
|
InteractiveElement, IntoElement, ParentElement, Render, ScrollStrategy, SharedString,
|
||||||
WeakEntity, Window, div, px, relative, size,
|
StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, Window, div, px, relative,
|
||||||
|
size,
|
||||||
};
|
};
|
||||||
use gpui_base::{VirtualListScrollHandle, v_virtual_list};
|
use gpui_base::{VirtualListScrollHandle, v_virtual_list};
|
||||||
use nip29::{Discovery, GroupCandidate, GroupKey, GroupsEvent, GroupsRegistry};
|
use nip29::{Discovery, GroupCandidate, GroupKey, GroupsEvent, GroupsRegistry};
|
||||||
@@ -23,6 +24,7 @@ use ui::{Disableable, Icon, IconName, Sizable, StyledExt, WindowExtension, h_fle
|
|||||||
|
|
||||||
const TITLE: &str = "Browse";
|
const TITLE: &str = "Browse";
|
||||||
const INPUT_PLACEHOLDER: &str = "wss://relay.example.com";
|
const INPUT_PLACEHOLDER: &str = "wss://relay.example.com";
|
||||||
|
const HEADER_HEIGHT: f32 = 96.0;
|
||||||
const CARD_HEIGHT: f32 = 164.0;
|
const CARD_HEIGHT: f32 = 164.0;
|
||||||
const NO_DESCRIPTION: &str = "No description";
|
const NO_DESCRIPTION: &str = "No description";
|
||||||
|
|
||||||
@@ -43,7 +45,7 @@ pub struct BrowsePanel {
|
|||||||
discovery: Discovery,
|
discovery: Discovery,
|
||||||
/// Whether the discovery sync is still running
|
/// Whether the discovery sync is still running
|
||||||
discovering: bool,
|
discovering: bool,
|
||||||
/// The scroll position of the group list
|
/// The scroll position of the panel
|
||||||
scroll_handle: VirtualListScrollHandle,
|
scroll_handle: VirtualListScrollHandle,
|
||||||
/// Whether the relay is still answering
|
/// Whether the relay is still answering
|
||||||
loading: bool,
|
loading: bool,
|
||||||
@@ -222,8 +224,7 @@ impl BrowsePanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Join a browsed group and open it.
|
/// Join a browsed group and open it.
|
||||||
fn join(&self, key: GroupKey, window: &mut Window, cx: &mut App) {
|
fn join(dock: &WeakEntity<DockArea>, key: GroupKey, window: &mut Window, cx: &mut App) {
|
||||||
let dock = self.dock.clone();
|
|
||||||
let registry = GroupsRegistry::global(cx);
|
let registry = GroupsRegistry::global(cx);
|
||||||
|
|
||||||
registry.update(cx, |registry, cx| {
|
registry.update(cx, |registry, cx| {
|
||||||
@@ -235,7 +236,7 @@ impl BrowsePanel {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ui::dock::add_panel_to(
|
ui::dock::add_panel_to(
|
||||||
&dock,
|
dock,
|
||||||
PanelHandle::new(nip29_ui::init(group, window, cx)),
|
PanelHandle::new(nip29_ui::init(group, window, cx)),
|
||||||
DockPlacement::Center,
|
DockPlacement::Center,
|
||||||
window,
|
window,
|
||||||
@@ -243,67 +244,114 @@ impl BrowsePanel {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_discovered_rows(
|
/// The discovered groups the user has not joined yet.
|
||||||
&mut self,
|
fn discovered_cards(&self, cx: &App) -> Vec<GroupCandidate> {
|
||||||
range: Range<usize>,
|
let joined: Vec<GroupKey> = GroupsRegistry::global(cx)
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Vec<AnyElement> {
|
|
||||||
let registry = GroupsRegistry::global(cx);
|
|
||||||
|
|
||||||
let joined: Vec<GroupKey> = registry
|
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.groups()
|
.groups()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|group| group.read(cx).key().clone())
|
.map(|group| group.read(cx).key().clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let cards: Vec<GroupCandidate> = self
|
self.discovery
|
||||||
.discovery
|
|
||||||
.discovered()
|
.discovered()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|candidate| !joined.contains(&candidate.key))
|
.filter(|candidate| !joined.contains(&candidate.key))
|
||||||
.skip(range.start * 2)
|
|
||||||
.take((range.end - range.start) * 2)
|
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect()
|
||||||
|
|
||||||
self.render_card_rows(&cards, cx)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_browse_rows(
|
/// The panel header, item 0 of the list so it scrolls with the cards.
|
||||||
&mut self,
|
fn render_header(&self, cx: &Context<Self>) -> AnyElement {
|
||||||
range: Range<usize>,
|
h_flex()
|
||||||
cx: &mut Context<Self>,
|
.w_full()
|
||||||
) -> Vec<AnyElement> {
|
.px_4()
|
||||||
let start = range.start * 2;
|
.h_24()
|
||||||
let end = (range.end * 2).min(self.candidates.len());
|
.flex_shrink_0()
|
||||||
let cards = self.candidates.get(start..end).unwrap_or(&[]);
|
.justify_between()
|
||||||
|
.child(
|
||||||
self.render_card_rows(cards, cx)
|
v_flex()
|
||||||
|
.min_w_0()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_sm()
|
||||||
|
.font_semibold()
|
||||||
|
.line_height(relative(1.2))
|
||||||
|
.child("Public Groups"),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.truncate()
|
||||||
|
.text_xs()
|
||||||
|
.text_color(cx.theme().text_muted)
|
||||||
|
.map(|this| match &self.relay {
|
||||||
|
Some(relay) => {
|
||||||
|
this.child(SharedString::from(relay.as_str().to_owned()))
|
||||||
|
}
|
||||||
|
None => this.child(SharedString::from(
|
||||||
|
"Hosted on current connected relays",
|
||||||
|
)),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.w_full()
|
||||||
|
.max_w_64()
|
||||||
|
.px_1()
|
||||||
|
.rounded(cx.theme().radius)
|
||||||
|
.bg(cx.theme().surface_background)
|
||||||
|
.child(
|
||||||
|
Input::new(&self.input)
|
||||||
|
.appearance(false)
|
||||||
|
.small()
|
||||||
|
.text_sm()
|
||||||
|
.flex_1(),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("relay-browse")
|
||||||
|
.icon(IconName::ArrowRight)
|
||||||
|
.tooltip("Browse by Relay")
|
||||||
|
.ghost()
|
||||||
|
.small()
|
||||||
|
.on_click(cx.listener(|this, _event, window, cx| {
|
||||||
|
this.browse(window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_card_rows(
|
fn render_list_rows(&mut self, range: Range<usize>, cx: &mut Context<Self>) -> Vec<AnyElement> {
|
||||||
&self,
|
let discover = self.relay.is_none();
|
||||||
cards: &[GroupCandidate],
|
|
||||||
cx: &mut Context<Self>,
|
let cards: Vec<GroupCandidate> = if discover {
|
||||||
) -> Vec<AnyElement> {
|
self.discovered_cards(cx)
|
||||||
cards
|
} else {
|
||||||
.chunks(2)
|
self.candidates.clone()
|
||||||
.map(|pair| {
|
};
|
||||||
let first = pair.first();
|
|
||||||
let second = pair.get(1);
|
range
|
||||||
|
.map(|ix| {
|
||||||
|
if ix == 0 {
|
||||||
|
self.render_header(cx)
|
||||||
|
} else {
|
||||||
|
let pair = cards.chunks(2).nth(ix - 1).unwrap_or(&[]);
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.h(px(CARD_HEIGHT))
|
.h(px(CARD_HEIGHT))
|
||||||
.p_2()
|
.px_4()
|
||||||
.gap_2()
|
.pb_4()
|
||||||
.children(first.map(|card| self.render_card(card, cx)))
|
.gap_4()
|
||||||
.child(match second {
|
.children(pair.first().map(|card| self.render_card(card, cx)))
|
||||||
|
.child(match pair.get(1) {
|
||||||
Some(card) => self.render_card(card, cx),
|
Some(card) => self.render_card(card, cx),
|
||||||
None => div().flex_1().into_any_element(),
|
None => div().flex_1().into_any_element(),
|
||||||
})
|
})
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -337,6 +385,7 @@ impl BrowsePanel {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
} else {
|
} else {
|
||||||
let panel = cx.entity().downgrade();
|
let panel = cx.entity().downgrade();
|
||||||
|
let dock = self.dock.clone();
|
||||||
let clicked = key;
|
let clicked = key;
|
||||||
|
|
||||||
Button::new(format!("join-{seed}"))
|
Button::new(format!("join-{seed}"))
|
||||||
@@ -344,9 +393,9 @@ impl BrowsePanel {
|
|||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
.on_click(move |_event, window, cx| {
|
.on_click(move |_event, window, cx| {
|
||||||
let _ = panel.update(cx, |panel, cx| {
|
Self::join(&dock, clicked.clone(), window, cx);
|
||||||
panel.join(clicked.clone(), window, cx);
|
|
||||||
});
|
let _ = panel.update(cx, |_, cx| cx.notify());
|
||||||
})
|
})
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
};
|
};
|
||||||
@@ -435,7 +484,8 @@ impl Panel for BrowsePanel {
|
|||||||
|
|
||||||
fn title(&self, cx: &App) -> AnyElement {
|
fn title(&self, cx: &App) -> AnyElement {
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_1p5()
|
.gap_1()
|
||||||
|
.text_xs()
|
||||||
.child(
|
.child(
|
||||||
Icon::new(IconName::Compass)
|
Icon::new(IconName::Compass)
|
||||||
.small()
|
.small()
|
||||||
@@ -456,97 +506,58 @@ impl Focusable for BrowsePanel {
|
|||||||
|
|
||||||
impl Render for BrowsePanel {
|
impl Render for BrowsePanel {
|
||||||
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 {
|
||||||
let count = self.candidates.len();
|
let view = cx.entity();
|
||||||
let discover = self.relay.is_none();
|
let discover = self.relay.is_none();
|
||||||
|
|
||||||
let (discovered_count, discovering) = if discover {
|
let cards: Vec<GroupCandidate> = if discover {
|
||||||
let registry = GroupsRegistry::global(cx);
|
self.discovered_cards(cx)
|
||||||
|
|
||||||
let joined: Vec<GroupKey> = registry
|
|
||||||
.read(cx)
|
|
||||||
.groups()
|
|
||||||
.iter()
|
|
||||||
.map(|group| group.read(cx).key().clone())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let discovered_count = self
|
|
||||||
.discovery
|
|
||||||
.discovered()
|
|
||||||
.iter()
|
|
||||||
.filter(|candidate| !joined.contains(&candidate.key))
|
|
||||||
.count();
|
|
||||||
|
|
||||||
(discovered_count, self.discovering)
|
|
||||||
} else {
|
} else {
|
||||||
(0, false)
|
self.candidates.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
let body = if discover {
|
let rows = cards.len().div_ceil(2);
|
||||||
if discovered_count > 0 {
|
|
||||||
let rows = discovered_count.div_ceil(2);
|
let content: AnyElement = if rows > 0 {
|
||||||
let item_sizes = Rc::new(vec![size(px(0.), px(CARD_HEIGHT)); rows]);
|
let mut item_sizes = vec![size(px(0.), px(HEADER_HEIGHT))];
|
||||||
|
item_sizes.resize(rows + 1, size(px(0.), px(CARD_HEIGHT)));
|
||||||
|
|
||||||
v_virtual_list(
|
v_virtual_list(
|
||||||
cx.entity(),
|
view,
|
||||||
"discovered",
|
"groups",
|
||||||
item_sizes,
|
Rc::new(item_sizes),
|
||||||
|this, range, _, cx| this.render_discovered_rows(range, cx),
|
|this, range, _window, cx| this.render_list_rows(range, cx),
|
||||||
)
|
)
|
||||||
.track_scroll(&self.scroll_handle)
|
.track_scroll(&self.scroll_handle)
|
||||||
.h_full()
|
.h_full()
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
} else if discovering {
|
|
||||||
notice("Discovering groups…", cx)
|
|
||||||
} else {
|
} else {
|
||||||
notice("No groups found.", cx)
|
let text = if discover {
|
||||||
|
if self.discovering {
|
||||||
|
"Discovering groups…"
|
||||||
|
} else {
|
||||||
|
"No groups found."
|
||||||
}
|
}
|
||||||
} else if count > 0 {
|
|
||||||
let rows = count.div_ceil(2);
|
|
||||||
let item_sizes = Rc::new(vec![size(px(0.), px(CARD_HEIGHT)); rows]);
|
|
||||||
|
|
||||||
v_virtual_list(cx.entity(), "groups", item_sizes, |this, range, _, cx| {
|
|
||||||
this.render_browse_rows(range, cx)
|
|
||||||
})
|
|
||||||
.track_scroll(&self.scroll_handle)
|
|
||||||
.h_full()
|
|
||||||
.into_any_element()
|
|
||||||
} else if self.loading {
|
} else if self.loading {
|
||||||
notice("Loading groups…", cx)
|
"Loading groups…"
|
||||||
} else {
|
} else {
|
||||||
notice("No groups found.", cx)
|
"No groups found."
|
||||||
};
|
};
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.id("browse-content")
|
||||||
.size_full()
|
.size_full()
|
||||||
.child(
|
.overflow_y_scroll()
|
||||||
v_flex()
|
.track_scroll(&self.scroll_handle)
|
||||||
.h_32()
|
.child(self.render_header(cx))
|
||||||
.flex_shrink_0()
|
.child(notice(text, cx))
|
||||||
.items_center()
|
.into_any_element()
|
||||||
.justify_center()
|
};
|
||||||
.bg(cx.theme().surface_background)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.w_full()
|
|
||||||
.max_w_56()
|
|
||||||
.gap_1()
|
|
||||||
.child(Input::new(&self.input).cleanable(true).flex_1())
|
|
||||||
.child(Button::new("browse-go").label("Go").primary().on_click(
|
|
||||||
cx.listener(|this, _event, window, cx| {
|
|
||||||
this.browse(window, cx);
|
|
||||||
}),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
div()
|
||||||
.flex_1()
|
.size_full()
|
||||||
.min_h_0()
|
|
||||||
.relative()
|
.relative()
|
||||||
.px_2()
|
.child(content)
|
||||||
.child(body)
|
.child(Scrollbar::vertical(&self.scroll_handle))
|
||||||
.child(Scrollbar::vertical(&self.scroll_handle)),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user