add group management menu with metadata, invites, and members
This commit is contained in:
+203
-6
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user