simplify nip29 discovery and sync metadata via negentropy

This commit is contained in:
2026-10-07 11:54:33 +07:00
parent d0f02dfb4b
commit e472077941
6 changed files with 377 additions and 409 deletions
+63 -63
View File
@@ -5,7 +5,7 @@ use gpui::prelude::FluentBuilder;
use gpui::{
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ParentElement, Render, ScrollStrategy, SharedString, Styled, Subscription,
WeakEntity, Window, div, px, size,
WeakEntity, Window, div, px, relative, size,
};
use gpui_base::{VirtualListScrollHandle, v_virtual_list};
use nip29::{Discovery, GroupCandidate, GroupKey, GroupsEvent, GroupsRegistry};
@@ -24,9 +24,7 @@ use ui::{Disableable, Icon, IconName, Sizable, StyledExt, WindowExtension, h_fle
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 CARD_HEIGHT: f32 = 164.0;
const NO_DESCRIPTION: &str = "No description";
pub fn init(dock: WeakEntity<DockArea>, window: &mut Window, cx: &mut App) -> Entity<BrowsePanel> {
@@ -201,7 +199,7 @@ impl BrowsePanel {
.cloned()
.collect();
self.render_card_rows(&cards, range, cx)
self.render_card_rows(&cards, cx)
}
fn render_browse_rows(
@@ -213,20 +211,23 @@ impl BrowsePanel {
let end = (range.end * 2).min(self.candidates.len());
let cards = self.candidates.get(start..end).unwrap_or(&[]);
self.render_card_rows(cards, range, cx)
self.render_card_rows(cards, cx)
}
/// A row holds two group cards side by side.
///
/// The cards are the relative slice of the visible rows, so they pair up
/// in order, two per row.
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);
cards
.chunks(2)
.map(|pair| {
let first = pair.first();
let second = pair.get(1);
h_flex()
.w_full()
@@ -234,7 +235,12 @@ impl BrowsePanel {
.p_2()
.gap_2()
.children(first.map(|card| self.render_card(card, cx)))
.children(second.map(|card| self.render_card(card, cx)))
// Keep an odd last card in one column: an empty spacer
// takes the second slot so the card cannot stretch.
.child(match second {
Some(card) => self.render_card(card, cx),
None => div().flex_1().into_any_element(),
})
.into_any_element()
})
.collect()
@@ -261,7 +267,7 @@ impl BrowsePanel {
let joined = registry.read(cx).group(&key, cx).is_some();
let action = if joined {
Button::new(format!("browse-joined-{seed}"))
Button::new(format!("joined-{seed}"))
.label("Joined")
.small()
.ghost()
@@ -271,7 +277,7 @@ impl BrowsePanel {
let panel = cx.entity().downgrade();
let clicked = key;
Button::new(format!("browse-join-{seed}"))
Button::new(format!("join-{seed}"))
.label("Join")
.small()
.ghost()
@@ -290,37 +296,38 @@ impl BrowsePanel {
.overflow_hidden()
.border_1()
.border_color(cx.theme().border)
.rounded_lg()
.bg(cx.theme().surface_background)
.rounded(cx.theme().radius)
.child(
v_flex()
.flex_1()
.min_h_0()
.p_2()
.gap_1p5()
.gap_2()
.child(
h_flex()
.w_full()
.items_center()
.gap_1p5()
.gap_2()
.when(!hide_avatar, |this| {
this.child(
Avatar::from_source(picture)
.seed(seed)
.small()
.flex_shrink_0(),
)
this.child(Avatar::from_source(picture).seed(seed).flex_shrink_0())
})
.child(
v_flex()
.flex_1()
.min_w_0()
.child(div().text_sm().font_medium().truncate().child(name))
.child(
div()
.text_sm()
.truncate()
.font_semibold()
.line_height(relative(1.2))
.child(name),
)
.child(
div()
.truncate()
.text_xs()
.text_color(cx.theme().text_placeholder)
.truncate()
.child(relay),
),
)
@@ -328,44 +335,37 @@ impl BrowsePanel {
)
.child(
div()
.h(px(DESCRIPTION_HEIGHT))
.text_xs()
.min_w_0()
.text_sm()
.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)),
.text_ellipsis()
.line_clamp(3)
.map(|this| {
if let Some(about) = about {
this.child(about)
} else {
this.child(NO_DESCRIPTION)
}
}),
)
.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")),
.gap_1p5()
.child(Icon::new(IconName::Group))
.child(
div()
.text_sm()
.text_color(cx.theme().text_muted)
.child(SharedString::from(format!("{members} members"))),
),
),
)
.into_any_element()
}
}
fn notice(text: &'static str, cx: &App) -> AnyElement {
h_flex()
.size_full()
.justify_center()
.text_sm()
.text_color(cx.theme().text_muted)
.child(text)
.into_any_element()
}
impl Panel for BrowsePanel {
fn panel_id(&self) -> SharedString {
TITLE.into()
@@ -460,7 +460,7 @@ impl Render for BrowsePanel {
.child(
h_flex()
.gap_2()
.p_3()
.px_2()
.flex_shrink_0()
.child(Input::new(&self.input).cleanable(true).flex_1())
.child(
@@ -475,16 +475,6 @@ impl Render for BrowsePanel {
this.child(Indicator::new().small().color(cx.theme().icon_accent))
}),
)
.when(discover && discovered_count > 0, |this| {
this.child(
div()
.px_3()
.pb_1()
.text_xs()
.text_color(cx.theme().text_muted)
.child("Discovered groups"),
)
})
.child(
div()
.flex_1()
@@ -495,3 +485,13 @@ impl Render for BrowsePanel {
)
}
}
fn notice(text: &'static str, cx: &App) -> AnyElement {
h_flex()
.size_full()
.justify_center()
.text_sm()
.text_color(cx.theme().text_muted)
.child(text)
.into_any_element()
}