update member row
This commit is contained in:
+14
-13
@@ -32,7 +32,6 @@ mod details;
|
|||||||
mod invite;
|
mod invite;
|
||||||
mod message;
|
mod message;
|
||||||
mod metadata;
|
mod metadata;
|
||||||
mod users;
|
|
||||||
|
|
||||||
/// Ask the shell to show or hide the sidebar's information for a group.
|
/// Ask the shell to show or hide the sidebar's information for a group.
|
||||||
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
#[derive(Action, Clone, PartialEq, Eq, Deserialize)]
|
||||||
@@ -172,7 +171,6 @@ pub fn actions(group: WeakEntity<Group>, cx: &App) -> AnyElement {
|
|||||||
let info = group.clone();
|
let info = group.clone();
|
||||||
let edit = group.clone();
|
let edit = group.clone();
|
||||||
let invite = group.clone();
|
let invite = group.clone();
|
||||||
let members = group.clone();
|
|
||||||
let leave = group;
|
let leave = group;
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
@@ -199,7 +197,7 @@ pub fn actions(group: WeakEntity<Group>, cx: &App) -> AnyElement {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Button::new("invite")
|
Button::new("group-invite")
|
||||||
.icon(IconName::Invite)
|
.icon(IconName::Invite)
|
||||||
.tooltip("Invite people")
|
.tooltip("Invite people")
|
||||||
.ghost()
|
.ghost()
|
||||||
@@ -208,16 +206,6 @@ pub fn actions(group: WeakEntity<Group>, cx: &App) -> AnyElement {
|
|||||||
invite::open(invite.clone(), window, cx);
|
invite::open(invite.clone(), window, cx);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.child(
|
|
||||||
Button::new("members")
|
|
||||||
.icon(IconName::Group)
|
|
||||||
.tooltip("Members")
|
|
||||||
.ghost()
|
|
||||||
.small()
|
|
||||||
.on_click(move |_event, window, cx| {
|
|
||||||
users::open(members.clone(), window, cx);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
Button::new("leave")
|
Button::new("leave")
|
||||||
@@ -268,6 +256,19 @@ pub fn confirm_leave(group: WeakEntity<Group>, window: &mut Window, cx: &mut App
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove a member from the group and surface any relay error as a notification.
|
||||||
|
pub fn remove_member(
|
||||||
|
group: WeakEntity<Group>,
|
||||||
|
public_key: PublicKey,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
match group.update(cx, |group, cx| group.remove_user(public_key, cx)) {
|
||||||
|
Ok(task) => report(window, cx, task),
|
||||||
|
Err(error) => log::warn!("nip29: removing a group member failed: {error}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Await a group write and surface any relay error as a notification.
|
/// Await a group write and surface any relay error as a notification.
|
||||||
pub(crate) fn report(window: &mut Window, cx: &mut App, task: Task<Result<()>>) {
|
pub(crate) fn report(window: &mut Window, cx: &mut App, task: Task<Result<()>>) {
|
||||||
window
|
window
|
||||||
|
|||||||
@@ -1,207 +0,0 @@
|
|||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
AnyElement, App, AppContext, Entity, IntoElement, ParentElement, SharedString, Styled,
|
|
||||||
WeakEntity, Window, div, px,
|
|
||||||
};
|
|
||||||
use nip29::{Group, Role};
|
|
||||||
use nostr_sdk::prelude::*;
|
|
||||||
use person::PersonRegistry;
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
use ui::avatar::Avatar;
|
|
||||||
use ui::button::{Button, ButtonVariants};
|
|
||||||
use ui::input::InputState;
|
|
||||||
use ui::notification::Notification;
|
|
||||||
use ui::switch::Switch;
|
|
||||||
use ui::{Sizable, StyledExt, WindowExtension, divider, h_flex, v_flex};
|
|
||||||
|
|
||||||
use crate::{field, report};
|
|
||||||
|
|
||||||
pub(crate) fn open(group: WeakEntity<Group>, window: &mut Window, cx: &mut App) {
|
|
||||||
let input = cx.new(|cx| InputState::new(window, cx).placeholder("npub1…"));
|
|
||||||
let selected = cx.new(|_cx| Vec::<String>::new());
|
|
||||||
|
|
||||||
window.open_dialog(cx, move |this, _window, cx| {
|
|
||||||
let group = group.clone();
|
|
||||||
let input = input.clone();
|
|
||||||
let selected = selected.clone();
|
|
||||||
|
|
||||||
let admins = group
|
|
||||||
.read_with(cx, |group, _cx| group.admins().entries().to_vec())
|
|
||||||
.unwrap_or_default();
|
|
||||||
let roles = group
|
|
||||||
.read_with(cx, |group, _cx| group.roles().roles().to_vec())
|
|
||||||
.unwrap_or_default();
|
|
||||||
let chosen = selected.read(cx).clone();
|
|
||||||
|
|
||||||
this.width(px(460.))
|
|
||||||
.max_h(px(620.))
|
|
||||||
.show_close(true)
|
|
||||||
.title("Members and roles")
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_4()
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_2()
|
|
||||||
.child(div().text_sm().font_medium().child("Admins"))
|
|
||||||
.when(admins.is_empty(), |this| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.text_sm()
|
|
||||||
.text_color(cx.theme().text_placeholder)
|
|
||||||
.child("The relay lists no admins."),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.children(
|
|
||||||
admins
|
|
||||||
.iter()
|
|
||||||
.map(|(key, roles)| admin(key, roles, &group, cx)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(divider(cx))
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.gap_3()
|
|
||||||
.child(div().text_sm().font_medium().child("Add a member"))
|
|
||||||
.child(field("Public key", &input, cx))
|
|
||||||
.when(!roles.is_empty(), |this| {
|
|
||||||
this.child(
|
|
||||||
v_flex().gap_2().children(
|
|
||||||
roles
|
|
||||||
.iter()
|
|
||||||
.map(|role| role_switch(role, &selected, &chosen)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(Button::new("add-user").label("Add").small().on_click({
|
|
||||||
let group = group.clone();
|
|
||||||
let input = input.clone();
|
|
||||||
let selected = selected.clone();
|
|
||||||
|
|
||||||
move |_event, window, cx| {
|
|
||||||
let value = input.read(cx).value().trim().to_owned();
|
|
||||||
|
|
||||||
let public_key = match PublicKey::parse(&value) {
|
|
||||||
Ok(public_key) => public_key,
|
|
||||||
Err(error) => {
|
|
||||||
window.push_notification(
|
|
||||||
Notification::error(error.to_string()),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let roles = selected.read(cx).clone();
|
|
||||||
|
|
||||||
match group.update(cx, |group, cx| {
|
|
||||||
group.put_user(public_key, roles, cx)
|
|
||||||
}) {
|
|
||||||
Ok(task) => report(window, cx, task),
|
|
||||||
Err(error) => log::warn!(
|
|
||||||
"nip29: adding a group member failed: {error}"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
input.update(cx, |input, cx| {
|
|
||||||
input.set_value("", window, cx);
|
|
||||||
});
|
|
||||||
selected.update(cx, |selected, cx| {
|
|
||||||
selected.clear();
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn admin(
|
|
||||||
public_key: &PublicKey,
|
|
||||||
roles: &[String],
|
|
||||||
group: &WeakEntity<Group>,
|
|
||||||
cx: &App,
|
|
||||||
) -> AnyElement {
|
|
||||||
let persons = PersonRegistry::global(cx);
|
|
||||||
let person = persons.read(cx).get(public_key, cx);
|
|
||||||
let public_key = *public_key;
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.justify_between()
|
|
||||||
.gap_2()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.min_w_0()
|
|
||||||
.child(
|
|
||||||
Avatar::new(person.avatar())
|
|
||||||
.seed(person.avatar_seed())
|
|
||||||
.xsmall(),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.min_w_0()
|
|
||||||
.child(div().truncate().child(person.name()))
|
|
||||||
.when(!roles.is_empty(), |this| {
|
|
||||||
this.child(
|
|
||||||
div()
|
|
||||||
.truncate()
|
|
||||||
.text_xs()
|
|
||||||
.text_color(cx.theme().text_placeholder)
|
|
||||||
.child(SharedString::from(roles.join(", "))),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new(SharedString::from(format!("del-{}", public_key.to_hex())))
|
|
||||||
.label("Remove")
|
|
||||||
.ghost()
|
|
||||||
.small()
|
|
||||||
.on_click({
|
|
||||||
let group = group.clone();
|
|
||||||
|
|
||||||
move |_event, window, cx| match group
|
|
||||||
.update(cx, |group, cx| group.remove_user(public_key, cx))
|
|
||||||
{
|
|
||||||
Ok(task) => report(window, cx, task),
|
|
||||||
Err(error) => {
|
|
||||||
log::warn!("nip29: removing a group member failed: {error}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn role_switch(role: &Role, selected: &Entity<Vec<String>>, chosen: &[String]) -> AnyElement {
|
|
||||||
let name = role.name.clone();
|
|
||||||
let description = role.description.clone();
|
|
||||||
|
|
||||||
Switch::new(SharedString::from(format!("role-{}", role.name)))
|
|
||||||
.label(role.name.clone())
|
|
||||||
.when_some(description, |this, description| {
|
|
||||||
this.description(description)
|
|
||||||
})
|
|
||||||
.small()
|
|
||||||
.checked(chosen.contains(&role.name))
|
|
||||||
.on_click({
|
|
||||||
let selected = selected.clone();
|
|
||||||
|
|
||||||
move |next, _window, cx| {
|
|
||||||
selected.update(cx, |selected, cx| {
|
|
||||||
if *next {
|
|
||||||
if !selected.contains(&name) {
|
|
||||||
selected.push(name.clone());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selected.retain(|role| role != &name);
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,7 @@ use crate::Selectable;
|
|||||||
use crate::avatar::Avatar;
|
use crate::avatar::Avatar;
|
||||||
use crate::button::Button;
|
use crate::button::Button;
|
||||||
use crate::menu::PopupMenu;
|
use crate::menu::PopupMenu;
|
||||||
|
use crate::nav::Nav;
|
||||||
use crate::popover::{Popover, PopoverState};
|
use crate::popover::{Popover, PopoverState};
|
||||||
|
|
||||||
/// Builds the items of a popup menu on each render.
|
/// Builds the items of a popup menu on each render.
|
||||||
@@ -41,6 +42,8 @@ impl DropdownMenu for Button {}
|
|||||||
|
|
||||||
impl DropdownMenu for Avatar {}
|
impl DropdownMenu for Avatar {}
|
||||||
|
|
||||||
|
impl DropdownMenu for Nav {}
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct DropdownMenuPopover<T: Selectable + IntoElement + 'static> {
|
pub struct DropdownMenuPopover<T: Selectable + IntoElement + 'static> {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
|
|||||||
+26
-5
@@ -2,9 +2,9 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, ClickEvent, ElementId, InteractiveElement, IntoElement, ParentElement,
|
AnyElement, App, ClickEvent, ElementId, InteractiveElement, Interactivity, IntoElement,
|
||||||
RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement, Styled, Window,
|
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, StyleRefinement,
|
||||||
div,
|
Styled, Window, div,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
@@ -14,28 +14,43 @@ use crate::{Selectable, StyledExt, h_flex, v_flex};
|
|||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Nav {
|
pub struct Nav {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
|
interactivity: Interactivity,
|
||||||
style: StyleRefinement,
|
style: StyleRefinement,
|
||||||
prefix: Option<AnyElement>,
|
prefix: Option<AnyElement>,
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
suffix: Option<AnyElement>,
|
suffix: Option<AnyElement>,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
|
clickable: bool,
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Nav {
|
impl Nav {
|
||||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||||
|
let id = id.into();
|
||||||
|
let mut interactivity = Interactivity::default();
|
||||||
|
interactivity.element_id = Some(id.clone());
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id,
|
||||||
|
interactivity,
|
||||||
style: StyleRefinement::default(),
|
style: StyleRefinement::default(),
|
||||||
prefix: None,
|
prefix: None,
|
||||||
label: SharedString::default(),
|
label: SharedString::default(),
|
||||||
suffix: None,
|
suffix: None,
|
||||||
selected: false,
|
selected: false,
|
||||||
|
clickable: false,
|
||||||
on_click: None,
|
on_click: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Marks the row as clickable even when it has no `on_click` handler, so it
|
||||||
|
/// still shows the hover affordance. Used by rows whose click opens a menu.
|
||||||
|
pub fn clickable(mut self, clickable: bool) -> Self {
|
||||||
|
self.clickable = clickable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Sets the element shown before the label, such as an avatar or icon.
|
/// Sets the element shown before the label, such as an avatar or icon.
|
||||||
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
pub fn prefix(mut self, prefix: impl IntoElement) -> Self {
|
||||||
self.prefix = Some(prefix.into_any_element());
|
self.prefix = Some(prefix.into_any_element());
|
||||||
@@ -74,6 +89,12 @@ impl Selectable for Nav {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl InteractiveElement for Nav {
|
||||||
|
fn interactivity(&mut self) -> &mut Interactivity {
|
||||||
|
&mut self.interactivity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Styled for Nav {
|
impl Styled for Nav {
|
||||||
fn style(&mut self) -> &mut StyleRefinement {
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
&mut self.style
|
&mut self.style
|
||||||
@@ -82,7 +103,7 @@ impl Styled for Nav {
|
|||||||
|
|
||||||
impl RenderOnce for Nav {
|
impl RenderOnce for Nav {
|
||||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
let clickable = self.on_click.is_some();
|
let clickable = self.on_click.is_some() || self.clickable;
|
||||||
|
|
||||||
v_flex().w_full().h_10().child(
|
v_flex().w_full().h_10().child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
|
|||||||
use common::TimestampExt;
|
use common::TimestampExt;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, AppContext, Context, Div, Entity, EventEmitter, FocusHandle, Focusable,
|
AnyElement, App, AppContext, ClipboardItem, Context, Div, Entity, EventEmitter, FocusHandle,
|
||||||
InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, SharedString, Stateful,
|
Focusable, InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, SharedString,
|
||||||
Styled, StyledImage, Subscription, Task, UniformListScrollHandle, WeakEntity, Window, div, img,
|
Stateful, Styled, StyledImage, Subscription, Task, UniformListScrollHandle, WeakEntity, Window,
|
||||||
px, relative, retain_all, uniform_list,
|
div, img, px, relative, retain_all, uniform_list,
|
||||||
};
|
};
|
||||||
use nip29::{Group, GroupKey, GroupsEvent, GroupsRegistry, Membership};
|
use nip29::{Group, GroupKey, GroupsEvent, GroupsRegistry, Membership};
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
@@ -922,10 +922,14 @@ impl Sidebar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_group(&mut self, group: Entity<Group>, cx: &mut Context<Self>) -> AnyElement {
|
fn render_group(&mut self, group: Entity<Group>, cx: &mut Context<Self>) -> AnyElement {
|
||||||
let (banner, name, seed, picture, total_members, rows) = {
|
let admin = NostrRegistry::global(cx)
|
||||||
|
.read(cx)
|
||||||
|
.current_user()
|
||||||
|
.is_some_and(|me| group.read(cx).admins().contains(&me));
|
||||||
|
|
||||||
|
let (name, seed, picture, total_members, rows) = {
|
||||||
let group = group.read(cx);
|
let group = group.read(cx);
|
||||||
let admins = group.admins();
|
let admins = group.admins();
|
||||||
let metadata = group.metadata();
|
|
||||||
|
|
||||||
let mut admin_rows = Vec::new();
|
let mut admin_rows = Vec::new();
|
||||||
let mut member_rows = Vec::new();
|
let mut member_rows = Vec::new();
|
||||||
@@ -965,9 +969,6 @@ impl Sidebar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
(
|
(
|
||||||
metadata.and_then(|metadata| {
|
|
||||||
metadata.banner.clone().or_else(|| metadata.picture.clone())
|
|
||||||
}),
|
|
||||||
group.display_name(),
|
group.display_name(),
|
||||||
group.key().cache_tag(),
|
group.key().cache_tag(),
|
||||||
group.display_image(),
|
group.display_image(),
|
||||||
@@ -977,30 +978,13 @@ impl Sidebar {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let rows = Rc::new(rows);
|
let rows = Rc::new(rows);
|
||||||
|
let member_group = group.downgrade();
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.min_h_0()
|
.min_h_0()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.when_some(banner, |this, banner| {
|
|
||||||
this.child(
|
|
||||||
div().px_2().flex_shrink_0().child(
|
|
||||||
div()
|
|
||||||
.h_40()
|
|
||||||
.w_full()
|
|
||||||
.relative()
|
|
||||||
.overflow_hidden()
|
|
||||||
.rounded(cx.theme().radius_lg)
|
|
||||||
.child(
|
|
||||||
img(banner)
|
|
||||||
.size_full()
|
|
||||||
.rounded(cx.theme().radius_lg)
|
|
||||||
.object_fit(ObjectFit::Fill),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.px_2()
|
.px_2()
|
||||||
@@ -1008,7 +992,8 @@ impl Sidebar {
|
|||||||
.justify_between()
|
.justify_between()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.pl_0p5()
|
||||||
|
.gap_1()
|
||||||
.flex_shrink_1()
|
.flex_shrink_1()
|
||||||
.child(Avatar::from_source(picture).seed(seed).small())
|
.child(Avatar::from_source(picture).seed(seed).small())
|
||||||
.child(
|
.child(
|
||||||
@@ -1046,7 +1031,13 @@ impl Sidebar {
|
|||||||
"group-rows",
|
"group-rows",
|
||||||
rows.len(),
|
rows.len(),
|
||||||
cx.processor(move |this, range, _window, cx| {
|
cx.processor(move |this, range, _window, cx| {
|
||||||
this.render_group_rows(range, rows.as_slice(), cx)
|
this.render_group_rows(
|
||||||
|
range,
|
||||||
|
rows.as_slice(),
|
||||||
|
&member_group,
|
||||||
|
admin,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.track_scroll(&self.group_scroll)
|
.track_scroll(&self.group_scroll)
|
||||||
@@ -1062,6 +1053,8 @@ impl Sidebar {
|
|||||||
&self,
|
&self,
|
||||||
range: Range<usize>,
|
range: Range<usize>,
|
||||||
rows: &[GroupRow],
|
rows: &[GroupRow],
|
||||||
|
group: &WeakEntity<Group>,
|
||||||
|
admin: bool,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Vec<AnyElement> {
|
) -> Vec<AnyElement> {
|
||||||
rows.get(range)
|
rows.get(range)
|
||||||
@@ -1069,7 +1062,9 @@ impl Sidebar {
|
|||||||
.flatten()
|
.flatten()
|
||||||
.map(|row| match row {
|
.map(|row| match row {
|
||||||
GroupRow::Section(section) => self.section_row(*section, cx),
|
GroupRow::Section(section) => self.section_row(*section, cx),
|
||||||
GroupRow::Member { public_key, roles } => self.member_row(public_key, roles, cx),
|
GroupRow::Member { public_key, roles } => {
|
||||||
|
self.member_row(public_key, roles, group, admin, cx)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -1106,16 +1101,22 @@ impl Sidebar {
|
|||||||
&self,
|
&self,
|
||||||
public_key: &PublicKey,
|
public_key: &PublicKey,
|
||||||
roles: &[String],
|
roles: &[String],
|
||||||
|
group: &WeakEntity<Group>,
|
||||||
|
admin: bool,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||||
let persons = PersonRegistry::global(cx);
|
let persons = PersonRegistry::global(cx);
|
||||||
let person = persons.read(cx).get(public_key, cx);
|
let person = persons.read(cx).get(public_key, cx);
|
||||||
|
let public_key = *public_key;
|
||||||
|
|
||||||
|
let group = group.clone();
|
||||||
|
|
||||||
Nav::new(public_key.to_hex())
|
Nav::new(public_key.to_hex())
|
||||||
.label(person.name())
|
.label(person.name())
|
||||||
.text_sm()
|
.text_sm()
|
||||||
.font_medium()
|
.font_medium()
|
||||||
|
.clickable(true)
|
||||||
.when(!hide_avatar, |this| {
|
.when(!hide_avatar, |this| {
|
||||||
this.prefix(
|
this.prefix(
|
||||||
Avatar::new(person.avatar())
|
Avatar::new(person.avatar())
|
||||||
@@ -1133,10 +1134,29 @@ impl Sidebar {
|
|||||||
.child(SharedString::from(roles.join(", "))),
|
.child(SharedString::from(roles.join(", "))),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.on_click({
|
.dropdown_menu(move |menu, _window, _cx| {
|
||||||
let public_key = *public_key;
|
let menu = menu
|
||||||
|
.item(
|
||||||
|
PopupMenuItem::new("View profile").on_click(move |_, window, cx| {
|
||||||
|
profile::open(public_key, window, cx);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.item(PopupMenuItem::new("Copy").on_click(move |_, _, cx| {
|
||||||
|
let npub = public_key.to_bech32().unwrap_or_default();
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new_string(npub));
|
||||||
|
}));
|
||||||
|
|
||||||
move |_event, window, cx| profile::open(public_key, window, cx)
|
if !admin {
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.separator()
|
||||||
|
.item(PopupMenuItem::new("Remove").on_click({
|
||||||
|
let group = group.clone();
|
||||||
|
move |_event, window, cx| {
|
||||||
|
nip29_ui::remove_member(group.clone(), public_key, window, cx);
|
||||||
|
}
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-5
@@ -592,11 +592,13 @@ before they reach the consumer, so a replay cannot double-hydrate.
|
|||||||
reactions and admin pin/delete, a pins strip, a fork banner, and leave. Opening a
|
reactions and admin pin/delete, a pins strip, a fork banner, and leave. Opening a
|
||||||
group also fills the sidebar with the group itself — banner (the `39000` `banner`
|
group also fills the sidebar with the group itself — banner (the `39000` `banner`
|
||||||
tag, falling back to its `picture`), avatar and name, a row of info / edit / invite /
|
tag, falling back to its `picture`), avatar and name, a row of info / edit / invite /
|
||||||
members / leave buttons, and the collapsible `Admins`/`Members` lists. Members are
|
leave buttons, and the collapsible `Admins`/`Members` lists. Clicking a member opens
|
||||||
clickable and open a profile dialog. The panel has no header: the group's `about` lives in the
|
a dropdown menu (view profile, copy npub, and — for admins — remove from group).
|
||||||
details dialog, and the composer's leading button shows or hides the sidebar's
|
There is no members-and-roles dialog: adding a member is done through the invite
|
||||||
group information. What a given account may actually do is the relay's call; the
|
flow, and roles are read-only. The panel has no header: the group's `about` lives
|
||||||
client shows the refusal verbatim.
|
in the details dialog, and the composer's leading button shows or hides the
|
||||||
|
sidebar's group information. What a given account may actually do is the relay's
|
||||||
|
call; the client shows the refusal verbatim.
|
||||||
- **The composer uploads to the configured blossom server.** `GroupPanel`'s
|
- **The composer uploads to the configured blossom server.** `GroupPanel`'s
|
||||||
paperclip uploads one file at a time, unencrypted, and appends its URL to the
|
paperclip uploads one file at a time, unencrypted, and appends its URL to the
|
||||||
message on send — the same plain-attachment path the chat composer uses when a
|
message on send — the same plain-attachment path the chat composer uses when a
|
||||||
|
|||||||
Reference in New Issue
Block a user