update community sidebar

This commit is contained in:
2026-09-22 08:14:56 +07:00
parent 29f3c74d06
commit 7e7d13cbfc
9 changed files with 691 additions and 584 deletions
+58 -189
View File
@@ -3,12 +3,9 @@ use community::{ChannelId, ChatMessage, Community, CommunityEvent};
use gpui::prelude::FluentBuilder;
use gpui::{
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ListAlignment, ListState, ParentElement, Render, SharedString,
StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, Window, div, list, px,
IntoElement, ListAlignment, ListState, ParentElement, Render, SharedString, Styled,
Subscription, Task, WeakEntity, Window, div, list, px,
};
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use settings::AppSettings;
use smallvec::{SmallVec, smallvec};
use theme::ActiveTheme;
use ui::avatar::Avatar;
@@ -16,8 +13,8 @@ use ui::button::{Button, ButtonVariants};
use ui::dock::{Panel, PanelEvent};
use ui::input::{InputEvent, Textarea, TextareaState};
use ui::notification::Notification;
use ui::scroll::{ScrollableElement, Scrollbar};
use ui::{Icon, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
use ui::scroll::Scrollbar;
use ui::{IconName, Sizable, WindowExtension, h_flex, v_flex};
mod message;
@@ -64,7 +61,7 @@ impl CommunityPanel {
(
SharedString::from(format!("community-{}", community.id().to_hex())),
community.name(),
community.channels().first().map(|channel| channel.id),
community.active_channel(),
)
};
@@ -93,11 +90,15 @@ impl CommunityPanel {
CommunityEvent::Updated(_) => {
cx.defer_in(window, |this, window, cx| this.reload(window, cx));
}
// The sidebar picked another channel while the community was updating.
CommunityEvent::Channel(..) => {
cx.defer_in(window, |this, window, cx| this.load(window, cx));
}
CommunityEvent::Error(error) => {
window
.push_notification(Notification::error(error.clone()).autohide(false), cx);
}
CommunityEvent::Open(_) => {}
CommunityEvent::Open(_) | CommunityEvent::Close(_) => {}
},
));
@@ -118,9 +119,26 @@ impl CommunityPanel {
panel
}
/// The channel to show, following the community's selection.
fn resolve_channel(&mut self, cx: &App) -> Option<ChannelId> {
let channel = self
.community
.read_with(cx, |community, _cx| community.active_channel())
.ok()
.flatten();
if channel != self.channel {
self.channel = channel;
self.messages.clear();
self.list_state.reset(0);
}
channel
}
/// Page the selected channel's history into the cache, then read it back.
fn load(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(channel) = self.channel else {
let Some(channel) = self.resolve_channel(cx) else {
return;
};
@@ -144,7 +162,7 @@ impl CommunityPanel {
/// Replace the timeline with the selected channel's folded messages.
fn reload(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let Some(channel) = self.channel else {
let Some(channel) = self.resolve_channel(cx) else {
return;
};
@@ -179,19 +197,6 @@ impl CommunityPanel {
}));
}
fn select_channel(&mut self, channel: ChannelId, window: &mut Window, cx: &mut Context<Self>) {
if self.channel == Some(channel) {
return;
}
self.channel = Some(channel);
self.messages.clear();
self.list_state.reset(0);
cx.notify();
self.load(window, cx);
}
fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let content = self.input.read(cx).value().trim().to_owned();
@@ -200,7 +205,7 @@ impl CommunityPanel {
return;
}
let Some(channel) = self.channel else {
let Some(channel) = self.resolve_channel(cx) else {
return;
};
@@ -238,88 +243,6 @@ impl CommunityPanel {
}));
}
fn render_channel(
&self,
id: ChannelId,
name: &str,
private: bool,
cx: &mut Context<Self>,
) -> AnyElement {
let selected = self.channel == Some(id);
h_flex()
.id(SharedString::from(format!(
"community-channel-{}",
id.to_hex()
)))
.w_full()
.h_8()
.flex_shrink_0()
.gap_2()
.px_2()
.rounded(cx.theme().radius)
.cursor_pointer()
.when(selected, |this| this.bg(cx.theme().ghost_element_selected))
.hover(|this| this.bg(cx.theme().ghost_element_hover))
.child(
Icon::new(if private {
IconName::Lock
} else {
IconName::Message
})
.small()
.text_color(cx.theme().icon_muted),
)
.child(
div()
.flex_1()
.min_w_0()
.text_ellipsis()
.child(SharedString::from(name.to_owned())),
)
.on_click(cx.listener(move |this, _event, window, cx| {
this.select_channel(id, window, cx);
}))
.into_any_element()
}
fn render_timeline(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.flex_1()
.min_w_0()
.h_full()
.child(
v_flex()
.flex_1()
.min_h_0()
.relative()
.map(|this| {
if self.messages.is_empty() {
this.child(
h_flex()
.size_full()
.justify_center()
.text_sm()
.text_color(cx.theme().text_placeholder)
.child("No messages yet"),
)
} else {
this.child(
list(
self.list_state.clone(),
cx.processor(move |this, ix, window, cx| {
this.render_message(ix, window, cx)
}),
)
.size_full(),
)
}
})
.child(Scrollbar::vertical(&self.list_state)),
)
.child(self.render_composer(cx))
}
fn render_message(
&mut self,
ix: usize,
@@ -329,7 +252,6 @@ impl CommunityPanel {
let Some(message) = self.messages.get(ix) else {
return div().into_any_element();
};
message::render(ix, message, cx)
}
@@ -340,8 +262,6 @@ impl CommunityPanel {
.p_2()
.gap_1()
.items_end()
.border_t_1()
.border_color(cx.theme().border)
.child(Textarea::new(&self.input).appearance(false).flex_1())
.child(
Button::new("send")
@@ -394,89 +314,38 @@ impl Focusable for CommunityPanel {
impl Render for CommunityPanel {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let Some(community) = self.community.upgrade() else {
return div().size_full();
};
let (channels, members) = {
let community = community.read(cx);
(
community
.channels()
.iter()
.map(|channel| (channel.id, channel.name.clone(), channel.private))
.collect::<Vec<_>>(),
community.members().iter().copied().collect::<Vec<_>>(),
)
};
v_flex()
.size_full()
.flex_row()
.min_w_0()
.child(
v_flex()
.id("community-sidebar")
.w(px(220.))
.h_full()
.flex_shrink_0()
.gap_1()
.p_2()
.border_r_1()
.border_color(cx.theme().border)
.overflow_y_scrollbar()
.child(section_label("Channels", cx))
.children(
channels.iter().map(|(id, name, private)| {
self.render_channel(*id, name, *private, cx)
}),
)
.child(section_label("Members", cx))
.children(
members
.iter()
.map(|public_key| render_member(*public_key, cx)),
),
.flex_1()
.min_h_0()
.relative()
.map(|this| {
if self.messages.is_empty() {
this.child(
h_flex()
.size_full()
.justify_center()
.text_sm()
.text_color(cx.theme().text_placeholder)
.child("No messages yet"),
)
} else {
this.child(
list(
self.list_state.clone(),
cx.processor(move |this, ix, window, cx| {
this.render_message(ix, window, cx)
}),
)
.size_full(),
)
}
})
.child(Scrollbar::vertical(&self.list_state)),
)
.child(self.render_timeline(cx))
.child(self.render_composer(cx))
}
}
fn section_label(label: &str, cx: &App) -> impl IntoElement {
div()
.px_2()
.pt_2()
.pb_1()
.text_xs()
.font_semibold()
.text_color(cx.theme().text_muted)
.child(SharedString::from(label.to_owned()))
}
fn render_member(public_key: PublicKey, cx: &App) -> AnyElement {
let persons = PersonRegistry::global(cx);
let person = persons.read(cx).get(&public_key, cx);
let hide_avatar = AppSettings::get_hide_avatar(cx);
h_flex()
.w_full()
.h_8()
.flex_shrink_0()
.gap_2()
.px_2()
.when(!hide_avatar, |this| {
this.child(
Avatar::new(person.avatar())
.seed(person.avatar_seed())
.xsmall(),
)
})
.child(
div()
.flex_1()
.min_w_0()
.text_ellipsis()
.child(person.name()),
)
.into_any_element()
}
+2 -4
View File
@@ -70,15 +70,13 @@ pub(crate) fn render(ix: usize, message: &ChatMessage, cx: &App) -> AnyElement {
fn content(message: &ChatMessage, cx: &App) -> AnyElement {
if message.deleted {
return div()
.text_sm()
.text_color(cx.theme().text_placeholder)
.text_color(cx.theme().text_danger)
.child("Message deleted")
.into_any_element();
}
div()
.text_sm()
.child(SharedString::from(message.content.clone()))
.child(SharedString::from(&message.content))
.into_any_element()
}