add group management menu with metadata, invites, and members

This commit is contained in:
2026-09-30 17:10:27 +07:00
parent ce1cff83ff
commit 8c7c672675
7 changed files with 724 additions and 43 deletions
+203 -6
View File
@@ -5,11 +5,11 @@ use chat::Message;
use chat_ui::text::rendered_text;
use gpui::prelude::FluentBuilder;
use gpui::{
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, FollowMode,
IntoElement, ListAlignment, ListScrollEvent, ListState, ParentElement, Render, SharedString,
Styled, Subscription, Task, WeakEntity, Window, div, list, px,
AnyElement, App, AppContext, ClipboardItem, Context, Div, Entity, EventEmitter, FocusHandle,
Focusable, FollowMode, IntoElement, ListAlignment, ListScrollEvent, ListState, ParentElement,
Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, div, list, px,
};
use nip29::{Group, GroupEvent, GroupsRegistry, Membership, Pin};
use nip29::{Group, GroupEvent, GroupMetadata, GroupsRegistry, Membership, Pin};
use nostr_sdk::prelude::*;
use person::PersonRegistry;
use smallvec::{SmallVec, smallvec};
@@ -19,15 +19,19 @@ use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariant, ButtonVariants};
use ui::dialog::DialogButtonProps;
use ui::dock::{Panel, PanelEvent};
use ui::input::{InputEvent, Textarea, TextareaState};
use ui::input::{Input, InputEvent, InputState, Textarea, TextareaState};
use ui::markdown::RenderedText;
use ui::menu::{DropdownMenu, PopupMenuItem};
use ui::message::WelcomeMessage;
use ui::notification::Notification;
use ui::scroll::Scrollbar;
use ui::{Disableable, Icon, IconName, Sizable, StyledExt, WindowExtension, h_flex, v_flex};
mod details;
mod invite;
mod message;
mod metadata;
mod users;
const LOAD_OLDER_THRESHOLD: usize = 20;
const RUN_WINDOW_SECS: u64 = 300;
@@ -105,6 +109,15 @@ 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 field(label: &'static str, input: &Entity<InputState>, cx: &App) -> Div {
v_flex()
.gap_1()
.text_sm()
.text_color(cx.theme().text_muted)
.child(label)
.child(Input::new(input))
}
pub(crate) fn pin_label(group: &Group, pin: &Pin) -> SharedString {
match pin {
Pin::Address(coordinate) => SharedString::from(format!("{coordinate}")),
@@ -302,6 +315,143 @@ impl GroupPanel {
.and_then(|group| Notice::of(group.read(cx)))
}
fn edit_metadata(
&mut self,
metadata: GroupMetadata,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Ok(edit) = self
.group
.update(cx, |group, cx| group.edit_metadata(&metadata, cx))
else {
return;
};
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
if let Err(error) = edit.await {
this.update_in(cx, |_this, window, cx| {
window.push_notification(Notification::error(error.to_string()), cx);
})?;
}
Ok(())
}));
}
fn create_invite(&mut self, code: String, window: &mut Window, cx: &mut Context<Self>) {
let Some(group) = self.group.upgrade() else {
return;
};
let link = {
let group = group.read(cx);
group.metadata().and_then(|metadata| {
metadata
.naddr(group.key().relay(), Some(&code))
.ok()
.map(SharedString::from)
})
};
let Some(link) = link else {
window.push_notification(
Notification::error(
"The relay doesn't publish group details, so no invite link can be made",
),
cx,
);
return;
};
let Ok(invite) = self
.group
.update(cx, |group, cx| group.create_invite(&code, cx))
else {
return;
};
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
if let Err(error) = invite.await {
this.update_in(cx, |_this, window, cx| {
window.push_notification(Notification::error(error.to_string()), cx);
})?;
return Ok(());
}
this.update_in(cx, |_this, window, cx| {
cx.write_to_clipboard(ClipboardItem::new_string(link.to_string()));
window.push_notification(Notification::success("Invite link copied"), cx);
})?;
Ok(())
}));
}
fn put_user(
&mut self,
public_key: PublicKey,
roles: Vec<String>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Ok(put) = self
.group
.update(cx, |group, cx| group.put_user(public_key, roles, cx))
else {
return;
};
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
if let Err(error) = put.await {
this.update_in(cx, |_this, window, cx| {
window.push_notification(Notification::error(error.to_string()), cx);
})?;
}
Ok(())
}));
}
fn remove_user(&mut self, public_key: PublicKey, window: &mut Window, cx: &mut Context<Self>) {
let Ok(remove) = self
.group
.update(cx, |group, cx| group.remove_user(public_key, cx))
else {
return;
};
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
if let Err(error) = remove.await {
this.update_in(cx, |_this, window, cx| {
window.push_notification(Notification::error(error.to_string()), cx);
})?;
}
Ok(())
}));
}
fn react(
&mut self,
group: WeakEntity<Group>,
id: EventId,
emoji: String,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Ok(react) = group.update(cx, |group, cx| group.send_reaction(id, emoji, cx)) else {
return;
};
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
if let Err(error) = react.await {
this.update_in(cx, |_this, window, cx| {
window.push_notification(Notification::error(error.to_string()), cx);
})?;
}
Ok(())
}));
}
fn delete_message(
&mut self,
group: WeakEntity<Group>,
@@ -396,15 +546,22 @@ impl GroupPanel {
return div().into_any_element();
};
let (name, picture, seed) = {
let (name, picture, seed, admin) = {
let group = group.read(cx);
let admin = NostrRegistry::global(cx)
.read(cx)
.current_user()
.is_some_and(|me| group.admins().contains(&me));
(
group.display_name(),
group.display_image(),
group.key().cache_tag(),
admin,
)
};
let panel = cx.weak_entity();
let details_group = self.group.clone();
let leave_group = self.group.clone();
@@ -436,6 +593,46 @@ impl GroupPanel {
details::open(details_group.clone(), window, cx);
}),
)
.when(admin, |this| {
this.child(
Button::new("group-manage")
.icon(IconName::Ellipsis)
.tooltip("Manage")
.ghost()
.small()
.dropdown_menu({
let group = self.group.clone();
move |menu, _window, _cx| {
menu.item(PopupMenuItem::new("Edit group").on_click({
let group = group.clone();
let panel = panel.clone();
move |_event, window, cx| {
metadata::open(group.clone(), panel.clone(), window, cx);
}
}))
.item(PopupMenuItem::new("Invite people").on_click({
let panel = panel.clone();
move |_event, window, cx| {
invite::open(panel.clone(), window, cx);
}
}))
.item(
PopupMenuItem::new("Members and roles").on_click({
let group = group.clone();
let panel = panel.clone();
move |_event, window, cx| {
users::open(group.clone(), panel.clone(), window, cx);
}
}),
)
}
}),
)
})
.child(
Button::new("group-leave")
.icon(IconName::Door)