update browse panel
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
use std::ops::Range;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, ParentElement, Render, SharedString, Styled, Subscription, WeakEntity, Window,
|
||||
div, uniform_list,
|
||||
IntoElement, ParentElement, Render, ScrollStrategy, SharedString, Styled, Subscription,
|
||||
WeakEntity, Window, div, px, size,
|
||||
};
|
||||
use gpui_base::{VirtualListScrollHandle, v_virtual_list};
|
||||
use nip29::{Discovery, GroupCandidate, GroupKey, GroupsEvent, GroupsRegistry};
|
||||
use nostr_sdk::prelude::RelayUrl;
|
||||
use settings::AppSettings;
|
||||
@@ -17,11 +19,16 @@ use ui::dock::{DockArea, DockPlacement, Panel, PanelEvent, PanelHandle};
|
||||
use ui::indicator::Indicator;
|
||||
use ui::input::{Input, InputEvent, InputState};
|
||||
use ui::notification::Notification;
|
||||
use ui::scroll::Scrollbar;
|
||||
use ui::{Disableable, Icon, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||
|
||||
const TITLE: &str = "Browse";
|
||||
const INPUT_PLACEHOLDER: &str = "wss://relay.example.com";
|
||||
|
||||
const CARD_HEIGHT: f32 = 150.0;
|
||||
const DESCRIPTION_HEIGHT: f32 = 32.0;
|
||||
const NO_DESCRIPTION: &str = "No description";
|
||||
|
||||
pub fn init(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut App) -> Entity<BrowsePanel> {
|
||||
cx.new(|cx| BrowsePanel::new(dock, window, cx))
|
||||
}
|
||||
@@ -37,6 +44,8 @@ pub struct BrowsePanel {
|
||||
candidates: Vec<GroupCandidate>,
|
||||
/// The groups discovered from the cache and the relays
|
||||
discovery: Entity<Discovery>,
|
||||
/// The scroll position of the group list
|
||||
scroll_handle: VirtualListScrollHandle,
|
||||
/// Whether the relay is still answering
|
||||
loading: bool,
|
||||
_subscriptions: SmallVec<[Subscription; 3]>,
|
||||
@@ -88,6 +97,7 @@ impl BrowsePanel {
|
||||
relay: None,
|
||||
candidates: Vec::new(),
|
||||
discovery,
|
||||
scroll_handle: VirtualListScrollHandle::new(),
|
||||
loading: false,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
@@ -113,6 +123,7 @@ impl BrowsePanel {
|
||||
self.relay = Some(relay.clone());
|
||||
self.candidates.clear();
|
||||
self.loading = true;
|
||||
self.scroll_handle.scroll_to_item(0, ScrollStrategy::Top);
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.browse(relay, cx);
|
||||
@@ -165,16 +176,12 @@ impl BrowsePanel {
|
||||
);
|
||||
}
|
||||
|
||||
fn render_rows(
|
||||
&self,
|
||||
candidates: &[GroupCandidate],
|
||||
relay_detail: bool,
|
||||
fn render_discovered_rows(
|
||||
&mut self,
|
||||
range: Range<usize>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<AnyElement> {
|
||||
let registry = GroupsRegistry::global(cx);
|
||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||
let panel = cx.entity().downgrade();
|
||||
|
||||
let joined: Vec<GroupKey> = registry
|
||||
.read(cx)
|
||||
@@ -183,81 +190,170 @@ impl BrowsePanel {
|
||||
.map(|group| group.read(cx).key().clone())
|
||||
.collect();
|
||||
|
||||
candidates
|
||||
.get(range)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|candidate| {
|
||||
let key = candidate.key.clone();
|
||||
let name = SharedString::from(candidate.display_name().to_owned());
|
||||
let cards: Vec<GroupCandidate> = self
|
||||
.discovery
|
||||
.read(cx)
|
||||
.discovered()
|
||||
.iter()
|
||||
.filter(|candidate| !joined.contains(&candidate.key))
|
||||
.skip(range.start * 2)
|
||||
.take((range.end - range.start) * 2)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let seed = key.cache_tag();
|
||||
let is_joined = joined.contains(&key);
|
||||
self.render_card_rows(&cards, range, cx)
|
||||
}
|
||||
|
||||
let picture = candidate
|
||||
.display_image()
|
||||
.map(|picture| SharedString::from(picture.to_owned()));
|
||||
fn render_browse_rows(
|
||||
&mut self,
|
||||
range: Range<usize>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<AnyElement> {
|
||||
let start = range.start * 2;
|
||||
let end = (range.end * 2).min(self.candidates.len());
|
||||
let cards = self.candidates.get(start..end).unwrap_or(&[]);
|
||||
|
||||
let detail = if relay_detail {
|
||||
key.relay().domain().unwrap_or_else(|| key.relay().as_str())
|
||||
} else {
|
||||
key.id().as_str()
|
||||
};
|
||||
self.render_card_rows(cards, range, cx)
|
||||
}
|
||||
|
||||
let action = if is_joined {
|
||||
Button::new(format!("browse-joined-{seed}"))
|
||||
.label("Joined")
|
||||
.small()
|
||||
.disabled(true)
|
||||
.into_any_element()
|
||||
} else {
|
||||
let panel = panel.clone();
|
||||
let clicked = key.clone();
|
||||
|
||||
Button::new(format!("browse-join-{seed}"))
|
||||
.label("Join")
|
||||
.small()
|
||||
.on_click(move |_event, window, cx| {
|
||||
if let Err(error) = panel.update(cx, |panel, cx| {
|
||||
panel.join(clicked.clone(), window, cx);
|
||||
}) {
|
||||
log::error!("Failed to join a browsed group: {error}");
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
};
|
||||
/// A row holds two group cards side by side.
|
||||
fn render_card_rows(
|
||||
&self,
|
||||
cards: &[GroupCandidate],
|
||||
range: Range<usize>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<AnyElement> {
|
||||
range
|
||||
.map(|row| {
|
||||
let first = cards.get(row * 2);
|
||||
let second = cards.get(row * 2 + 1);
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.h(px(CARD_HEIGHT))
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.px_1p5()
|
||||
.py_1()
|
||||
.when(!hide_avatar, |this| {
|
||||
this.child(
|
||||
Avatar::from_source(picture)
|
||||
.seed(seed)
|
||||
.small()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.child(div().text_sm().font_medium().truncate().child(name))
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.truncate()
|
||||
.child(SharedString::from(detail.to_owned())),
|
||||
),
|
||||
)
|
||||
.child(action)
|
||||
.children(first.map(|card| self.render_card(card, cx)))
|
||||
.children(second.map(|card| self.render_card(card, cx)))
|
||||
.into_any_element()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_card(&self, candidate: &GroupCandidate, cx: &mut Context<Self>) -> AnyElement {
|
||||
let key = candidate.key.clone();
|
||||
let seed = key.cache_tag();
|
||||
let members = self.discovery.read(cx).members(&key).unwrap_or(0);
|
||||
|
||||
let name = SharedString::from(candidate.display_name().to_owned());
|
||||
let relay = SharedString::from(key.relay().as_str().to_owned());
|
||||
|
||||
let picture = candidate
|
||||
.display_image()
|
||||
.map(|picture| SharedString::from(picture.to_owned()));
|
||||
|
||||
let about = candidate
|
||||
.display_about()
|
||||
.map(|about| SharedString::from(about.to_owned()));
|
||||
|
||||
let registry = GroupsRegistry::global(cx);
|
||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||
let joined = registry.read(cx).group(&key, cx).is_some();
|
||||
|
||||
let action = if joined {
|
||||
Button::new(format!("browse-joined-{seed}"))
|
||||
.label("Joined")
|
||||
.small()
|
||||
.ghost()
|
||||
.disabled(true)
|
||||
.into_any_element()
|
||||
} else {
|
||||
let panel = cx.entity().downgrade();
|
||||
let clicked = key;
|
||||
|
||||
Button::new(format!("browse-join-{seed}"))
|
||||
.label("Join")
|
||||
.small()
|
||||
.ghost()
|
||||
.on_click(move |_event, window, cx| {
|
||||
let _ = panel.update(cx, |panel, cx| {
|
||||
panel.join(clicked.clone(), window, cx);
|
||||
});
|
||||
})
|
||||
.into_any_element()
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.h_full()
|
||||
.overflow_hidden()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.rounded_lg()
|
||||
.bg(cx.theme().surface_background)
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.p_2()
|
||||
.gap_1p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.gap_1p5()
|
||||
.when(!hide_avatar, |this| {
|
||||
this.child(
|
||||
Avatar::from_source(picture)
|
||||
.seed(seed)
|
||||
.small()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.child(div().text_sm().font_medium().truncate().child(name))
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.truncate()
|
||||
.child(relay),
|
||||
),
|
||||
)
|
||||
.child(action),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.h(px(DESCRIPTION_HEIGHT))
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.when_none(&about, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.child(NO_DESCRIPTION),
|
||||
)
|
||||
})
|
||||
.when_some(about, |this, about| this.child(about)),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.child(Icon::new(IconName::Group).text_color(cx.theme().icon_muted))
|
||||
.child(format!("{members} members")),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn notice(text: &'static str, cx: &App) -> AnyElement {
|
||||
@@ -301,8 +397,11 @@ impl Render for BrowsePanel {
|
||||
let count = self.candidates.len();
|
||||
let discover = self.relay.is_none();
|
||||
|
||||
let (discovered, discovered_count, discovering) = if discover {
|
||||
let (discovered_count, discovering) = if discover {
|
||||
let registry = GroupsRegistry::global(cx);
|
||||
let discovery = self.discovery.read(cx);
|
||||
let discovering = discovery.loading();
|
||||
|
||||
let joined: Vec<GroupKey> = registry
|
||||
.read(cx)
|
||||
.groups()
|
||||
@@ -310,31 +409,29 @@ impl Render for BrowsePanel {
|
||||
.map(|group| group.read(cx).key().clone())
|
||||
.collect();
|
||||
|
||||
let discovery = self.discovery.read(cx);
|
||||
let discovering = discovery.loading();
|
||||
|
||||
let discovered: Vec<GroupCandidate> = discovery
|
||||
let discovered_count = discovery
|
||||
.discovered()
|
||||
.iter()
|
||||
.filter(|candidate| !joined.contains(&candidate.key))
|
||||
.cloned()
|
||||
.collect();
|
||||
.count();
|
||||
|
||||
let count = discovered.len();
|
||||
(discovered, count, discovering)
|
||||
(discovered_count, discovering)
|
||||
} else {
|
||||
(Vec::new(), 0, false)
|
||||
(0, false)
|
||||
};
|
||||
|
||||
let body = if discover {
|
||||
if discovered_count > 0 {
|
||||
uniform_list(
|
||||
"browse-discovered",
|
||||
discovered_count,
|
||||
cx.processor(move |this, range, _window, cx| {
|
||||
this.render_rows(&discovered, true, range, cx)
|
||||
}),
|
||||
let rows = discovered_count.div_ceil(2);
|
||||
let item_sizes = Rc::new(vec![size(px(0.), px(CARD_HEIGHT)); rows]);
|
||||
|
||||
v_virtual_list(
|
||||
cx.entity(),
|
||||
"discovered",
|
||||
item_sizes,
|
||||
|this, range, _, cx| this.render_discovered_rows(range, cx),
|
||||
)
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.h_full()
|
||||
.into_any_element()
|
||||
} else if discovering {
|
||||
@@ -343,13 +440,13 @@ impl Render for BrowsePanel {
|
||||
notice("No groups found.", cx)
|
||||
}
|
||||
} else if count > 0 {
|
||||
uniform_list(
|
||||
"browse-groups",
|
||||
count,
|
||||
cx.processor(|this, range, _window, cx| {
|
||||
this.render_rows(&this.candidates, false, range, cx)
|
||||
}),
|
||||
)
|
||||
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 {
|
||||
@@ -388,6 +485,13 @@ impl Render for BrowsePanel {
|
||||
.child("Discovered groups"),
|
||||
)
|
||||
})
|
||||
.child(div().flex_1().min_h_0().child(body))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.relative()
|
||||
.child(body)
|
||||
.child(Scrollbar::vertical(&self.scroll_handle)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user