add group header, moderation menu, and pins strip
This commit is contained in:
+274
-5
@@ -9,21 +9,24 @@ use gpui::{
|
||||
IntoElement, ListAlignment, ListScrollEvent, ListState, ParentElement, Render, SharedString,
|
||||
Styled, Subscription, Task, WeakEntity, Window, div, list, px,
|
||||
};
|
||||
use nip29::{Group, GroupEvent, GroupsRegistry, Membership};
|
||||
use nip29::{Group, GroupEvent, GroupsRegistry, Membership, Pin};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::PersonRegistry;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
use state::NostrRegistry;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::button::{Button, ButtonVariant, ButtonVariants};
|
||||
use ui::dialog::DialogButtonProps;
|
||||
use ui::dock::{Panel, PanelEvent};
|
||||
use ui::input::{InputEvent, Textarea, TextareaState};
|
||||
use ui::markdown::RenderedText;
|
||||
use ui::message::WelcomeMessage;
|
||||
use ui::notification::Notification;
|
||||
use ui::scroll::Scrollbar;
|
||||
use ui::{Disableable, IconName, Sizable, WindowExtension, h_flex, v_flex};
|
||||
use ui::{Disableable, Icon, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
|
||||
|
||||
mod details;
|
||||
mod message;
|
||||
|
||||
const LOAD_OLDER_THRESHOLD: usize = 20;
|
||||
@@ -102,6 +105,26 @@ pub fn init(group: Entity<Group>, window: &mut Window, cx: &mut App) -> Entity<G
|
||||
cx.new(|cx| GroupPanel::new(group, window, cx))
|
||||
}
|
||||
|
||||
pub(crate) fn pin_label(group: &Group, pin: &Pin) -> SharedString {
|
||||
match pin {
|
||||
Pin::Address(coordinate) => SharedString::from(format!("{coordinate}")),
|
||||
Pin::Event(id) => {
|
||||
let snippet = group
|
||||
.messages()
|
||||
.iter()
|
||||
.find(|message| message.id == *id)
|
||||
.and_then(|message| message.content.lines().next())
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty());
|
||||
|
||||
match snippet {
|
||||
Some(snippet) => SharedString::from(snippet.chars().take(48).collect::<String>()),
|
||||
None => SharedString::from(id.to_hex().chars().take(8).collect::<String>()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GroupPanel {
|
||||
id: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
@@ -279,6 +302,229 @@ impl GroupPanel {
|
||||
.and_then(|group| Notice::of(group.read(cx)))
|
||||
}
|
||||
|
||||
fn delete_message(
|
||||
&mut self,
|
||||
group: WeakEntity<Group>,
|
||||
id: EventId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Ok(delete) = group.update(cx, |group, cx| group.delete_event(id, cx)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
if let Err(error) = delete.await {
|
||||
this.update_in(cx, |_this, window, cx| {
|
||||
window.push_notification(Notification::error(error.to_string()), cx);
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
fn toggle_pin(
|
||||
&mut self,
|
||||
group: WeakEntity<Group>,
|
||||
id: EventId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Ok(update) = group.update(cx, |group, cx| {
|
||||
let mut pins = group.pins().pins().to_vec();
|
||||
|
||||
match pins.iter().position(|pin| *pin == Pin::Event(id)) {
|
||||
Some(ix) => {
|
||||
pins.remove(ix);
|
||||
}
|
||||
None => pins.push(Pin::Event(id)),
|
||||
}
|
||||
|
||||
group.update_pins(pins, cx)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
if let Err(error) = update.await {
|
||||
this.update_in(cx, |_this, window, cx| {
|
||||
window.push_notification(Notification::error(error.to_string()), cx);
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
fn confirm_leave(group: WeakEntity<Group>, window: &mut Window, cx: &mut App) {
|
||||
let Some(name) = group.read_with(cx, |group, _cx| group.display_name()).ok() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(key) = group.read_with(cx, |group, _cx| group.key().clone()).ok() else {
|
||||
return;
|
||||
};
|
||||
|
||||
window.open_dialog(cx, move |this, _window, _cx| {
|
||||
let key = key.clone();
|
||||
|
||||
this.confirm()
|
||||
.width(px(360.))
|
||||
.title(format!("Leave {name}?"))
|
||||
.button_props(
|
||||
DialogButtonProps::default()
|
||||
.cancel_text("Cancel")
|
||||
.ok_text("Leave")
|
||||
.ok_variant(ButtonVariant::Danger),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.child("You will stop receiving messages from this group."),
|
||||
)
|
||||
.on_ok(move |_event, _window, cx| {
|
||||
let registry = GroupsRegistry::global(cx);
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.leave(&key, cx);
|
||||
});
|
||||
true
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn render_header(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let Some(group) = self.group.upgrade() else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
let (name, picture, seed) = {
|
||||
let group = group.read(cx);
|
||||
(
|
||||
group.display_name(),
|
||||
group.display_image(),
|
||||
group.key().cache_tag(),
|
||||
)
|
||||
};
|
||||
|
||||
let details_group = self.group.clone();
|
||||
let leave_group = self.group.clone();
|
||||
|
||||
h_flex()
|
||||
.flex_shrink_0()
|
||||
.w_full()
|
||||
.h_10()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(Avatar::from_source(picture).seed(seed).small())
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.text_sm()
|
||||
.font_medium()
|
||||
.truncate()
|
||||
.child(name),
|
||||
)
|
||||
.child(
|
||||
Button::new("group-details")
|
||||
.icon(IconName::Info)
|
||||
.tooltip("Details")
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(move |_event, window, cx| {
|
||||
details::open(details_group.clone(), window, cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("group-leave")
|
||||
.icon(IconName::Door)
|
||||
.tooltip("Leave")
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(move |_event, window, cx| {
|
||||
Self::confirm_leave(leave_group.clone(), window, cx);
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_banner(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let Some(group) = self.group.upgrade() else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
let Some(moved) = group.read(cx).elsewhere().cloned() else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
let relay = moved.relay().clone();
|
||||
let message = SharedString::from(format!("This group may have moved to {relay}."));
|
||||
|
||||
h_flex()
|
||||
.flex_shrink_0()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.px_3()
|
||||
.py_2()
|
||||
.text_sm()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(
|
||||
Icon::new(IconName::Info)
|
||||
.small()
|
||||
.text_color(cx.theme().icon_muted),
|
||||
)
|
||||
.child(div().flex_1().min_w_0().truncate().child(message))
|
||||
.child(
|
||||
Button::new("group-moved")
|
||||
.label("Join")
|
||||
.small()
|
||||
.on_click(cx.listener(move |_this, _event, _window, cx| {
|
||||
let registry = GroupsRegistry::global(cx);
|
||||
|
||||
registry.update(cx, |registry, cx| {
|
||||
registry.join_key(moved.clone(), None, cx);
|
||||
});
|
||||
})),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_pins(&self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let Some(group) = self.group.upgrade() else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
let group = group.read(cx);
|
||||
let pins = group.pins().pins();
|
||||
|
||||
if pins.is_empty() {
|
||||
return div().into_any_element();
|
||||
}
|
||||
|
||||
h_flex()
|
||||
.flex_shrink_0()
|
||||
.w_full()
|
||||
.flex_wrap()
|
||||
.gap_1()
|
||||
.px_3()
|
||||
.py_1p5()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().border)
|
||||
.children(pins.iter().map(|pin| {
|
||||
div()
|
||||
.px_1p5()
|
||||
.py_0p5()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().element_background)
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(pin_label(group, pin))
|
||||
}))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_welcome(&self, cx: &App) -> AnyElement {
|
||||
let (name, avatar) = match self.group.upgrade() {
|
||||
Some(group) => {
|
||||
@@ -419,7 +665,7 @@ impl GroupPanel {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
let show_author = opens_run(messages, ix - 2);
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let persons = PersonRegistry::global(cx);
|
||||
|
||||
let content = self
|
||||
@@ -430,7 +676,27 @@ impl GroupPanel {
|
||||
})
|
||||
.element(ix.into(), window, cx);
|
||||
|
||||
message::render(ix, message, content, show_author, cx)
|
||||
let admin = nostr
|
||||
.read(cx)
|
||||
.current_user()
|
||||
.is_some_and(|me| group.admins().contains(&me));
|
||||
|
||||
let show_author = opens_run(messages, ix - 2);
|
||||
let pinned = group.pins().pins().contains(&Pin::Event(message.id));
|
||||
|
||||
message::render(
|
||||
ix,
|
||||
message,
|
||||
content,
|
||||
show_author,
|
||||
message::Actions {
|
||||
group: self.group.clone(),
|
||||
panel: cx.weak_entity(),
|
||||
admin,
|
||||
pinned,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
fn render_composer(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
@@ -512,6 +778,9 @@ impl Render for GroupPanel {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.min_w_0()
|
||||
.child(self.render_header(cx))
|
||||
.child(self.render_banner(cx))
|
||||
.child(self.render_pins(cx))
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
|
||||
Reference in New Issue
Block a user