add group activity rows to nip29 timeline
This commit is contained in:
@@ -52,6 +52,31 @@ pub enum SidebarRow {
|
||||
Contact { public_key: PublicKey },
|
||||
}
|
||||
|
||||
/// A collapsible section of the group view that replaces the tab list.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum GroupSection {
|
||||
Admins,
|
||||
Members,
|
||||
}
|
||||
|
||||
impl GroupSection {
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Admins => "Admins",
|
||||
Self::Members => "Members",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A row in the group view.
|
||||
enum GroupRow {
|
||||
Section(GroupSection),
|
||||
Member {
|
||||
public_key: PublicKey,
|
||||
roles: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct Sidebar {
|
||||
focus_handle: FocusHandle,
|
||||
scroll_handles: [UniformListScrollHandle; 2],
|
||||
@@ -60,6 +85,15 @@ pub struct Sidebar {
|
||||
banner: SharedString,
|
||||
active_tab: SidebarTab,
|
||||
|
||||
/// The group whose information the sidebar is browsing, if any.
|
||||
group: Option<WeakEntity<Group>>,
|
||||
/// Scroll state of the group's member list.
|
||||
group_scroll: UniformListScrollHandle,
|
||||
/// Whether the group view's admins section is expanded.
|
||||
admins_open: bool,
|
||||
/// Whether the group view's members section is expanded.
|
||||
members_open: bool,
|
||||
|
||||
/// Whether the inbox list is showing contacts to start a chat with.
|
||||
contacts_open: bool,
|
||||
/// The signed-in user's contacts, loaded when the contact picker opens.
|
||||
@@ -130,6 +164,10 @@ impl Sidebar {
|
||||
dock,
|
||||
banner: pick_banner(),
|
||||
active_tab: SidebarTab::Inbox,
|
||||
group: None,
|
||||
group_scroll: UniformListScrollHandle::new(),
|
||||
admins_open: true,
|
||||
members_open: true,
|
||||
contacts_open: false,
|
||||
contacts: None,
|
||||
selected_contacts: HashSet::new(),
|
||||
@@ -142,6 +180,20 @@ impl Sidebar {
|
||||
}
|
||||
}
|
||||
|
||||
/// Show the group view, replacing the tab list.
|
||||
fn show_group(&mut self, group: &Entity<Group>, cx: &mut Context<Self>) {
|
||||
self.group = Some(group.downgrade());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Leave the group view, returning the sidebar to its tab list.
|
||||
fn reset_group(&mut self, cx: &mut Context<Self>) {
|
||||
if self.group.take().is_none() {
|
||||
return;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_tab(&mut self, tab: SidebarTab, cx: &mut Context<Self>) {
|
||||
if self.active_tab == tab {
|
||||
return;
|
||||
@@ -378,6 +430,18 @@ impl Sidebar {
|
||||
.when_some(AutoUpdater::try_global(cx), |this, updater| {
|
||||
this.child(self.render_updater(updater, cx))
|
||||
})
|
||||
.when(self.group.is_some(), |this| {
|
||||
this.child(
|
||||
Button::new("sidebar-back")
|
||||
.icon(IconName::ArrowLeft)
|
||||
.tooltip("Back")
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(cx.listener(|this, _event, _window, cx| {
|
||||
this.reset_group(cx);
|
||||
})),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn render_updater(&self, updater: Entity<AutoUpdater>, cx: &mut App) -> AnyElement {
|
||||
@@ -768,6 +832,7 @@ impl Sidebar {
|
||||
}
|
||||
SidebarRow::Group { group } => {
|
||||
let dock = self.dock.clone();
|
||||
let sidebar = cx.entity().downgrade();
|
||||
let group = group.clone();
|
||||
|
||||
let name = group.read(cx).display_name();
|
||||
@@ -812,6 +877,12 @@ impl Sidebar {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
if let Err(error) = sidebar.update(cx, |this, cx| {
|
||||
this.show_group(&group, cx);
|
||||
}) {
|
||||
log::error!("Failed to show the group in the sidebar: {error}");
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -819,6 +890,191 @@ impl Sidebar {
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_group(&mut self, group: Entity<Group>, cx: &mut Context<Self>) -> AnyElement {
|
||||
let (banner, name, seed, picture, rows) = {
|
||||
let group = group.read(cx);
|
||||
let admins = group.admins();
|
||||
let mut admin_rows = Vec::new();
|
||||
let mut member_rows = Vec::new();
|
||||
|
||||
for (public_key, roles) in admins.entries() {
|
||||
admin_rows.push(GroupRow::Member {
|
||||
public_key: *public_key,
|
||||
roles: roles.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
for public_key in group.members().iter() {
|
||||
if admins.contains(public_key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
member_rows.push(GroupRow::Member {
|
||||
public_key: *public_key,
|
||||
roles: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut rows = Vec::new();
|
||||
|
||||
if !admin_rows.is_empty() {
|
||||
rows.push(GroupRow::Section(GroupSection::Admins));
|
||||
if self.admins_open {
|
||||
rows.append(&mut admin_rows);
|
||||
}
|
||||
}
|
||||
|
||||
if !member_rows.is_empty() {
|
||||
rows.push(GroupRow::Section(GroupSection::Members));
|
||||
if self.members_open {
|
||||
rows.append(&mut member_rows);
|
||||
}
|
||||
}
|
||||
|
||||
let metadata = group.metadata();
|
||||
|
||||
(
|
||||
metadata.and_then(|metadata| metadata.banner.clone()),
|
||||
group.display_name(),
|
||||
group.key().cache_tag(),
|
||||
group.display_image(),
|
||||
rows,
|
||||
)
|
||||
};
|
||||
|
||||
let rows = Rc::new(rows);
|
||||
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
.flex_shrink_0()
|
||||
.child(Avatar::from_source(picture).seed(seed).small())
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.text_sm()
|
||||
.font_medium()
|
||||
.truncate()
|
||||
.child(name),
|
||||
),
|
||||
)
|
||||
.when_some(banner, |this, banner| {
|
||||
this.child(
|
||||
div().px_2().flex_shrink_0().child(
|
||||
img(banner)
|
||||
.w_full()
|
||||
.h_20()
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.object_fit(ObjectFit::Cover),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.min_h_0()
|
||||
.flex_1()
|
||||
.child(
|
||||
uniform_list(
|
||||
"group-rows",
|
||||
rows.len(),
|
||||
cx.processor(move |this, range, _window, cx| {
|
||||
this.render_group_rows(range, rows.as_slice(), cx)
|
||||
}),
|
||||
)
|
||||
.track_scroll(&self.group_scroll)
|
||||
.h_full()
|
||||
.px_2(),
|
||||
)
|
||||
.child(Scrollbar::vertical(&self.group_scroll)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_group_rows(
|
||||
&self,
|
||||
range: Range<usize>,
|
||||
rows: &[GroupRow],
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<AnyElement> {
|
||||
rows.get(range)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|row| match row {
|
||||
GroupRow::Section(section) => self.section_row(*section, cx),
|
||||
GroupRow::Member { public_key, roles } => self.member_row(public_key, roles, cx),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn section_row(&self, section: GroupSection, cx: &mut Context<Self>) -> AnyElement {
|
||||
let open = match section {
|
||||
GroupSection::Admins => self.admins_open,
|
||||
GroupSection::Members => self.members_open,
|
||||
};
|
||||
let icon = if open {
|
||||
IconName::CaretDown
|
||||
} else {
|
||||
IconName::CaretRight
|
||||
};
|
||||
let label = section.label();
|
||||
|
||||
Nav::new(label)
|
||||
.label(label)
|
||||
.suffix(Icon::new(icon).small().text_color(cx.theme().icon_muted))
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||
match section {
|
||||
GroupSection::Admins => this.admins_open = !this.admins_open,
|
||||
GroupSection::Members => this.members_open = !this.members_open,
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn member_row(
|
||||
&self,
|
||||
public_key: &PublicKey,
|
||||
roles: &[String],
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||
let persons = PersonRegistry::global(cx);
|
||||
let person = persons.read(cx).get(public_key, cx);
|
||||
|
||||
Nav::new(public_key.to_hex())
|
||||
.label(person.name())
|
||||
.text_sm()
|
||||
.font_medium()
|
||||
.when(!hide_avatar, |this| {
|
||||
this.prefix(
|
||||
Avatar::new(person.avatar())
|
||||
.seed(person.avatar_seed())
|
||||
.small()
|
||||
.flex_shrink_0(),
|
||||
)
|
||||
})
|
||||
.when(!roles.is_empty(), |this| {
|
||||
this.suffix(
|
||||
div()
|
||||
.truncate()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.child(SharedString::from(roles.join(", "))),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for Sidebar {
|
||||
@@ -856,6 +1112,8 @@ impl Render for Sidebar {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let loading = chat.read(cx).loading();
|
||||
|
||||
let group = self.group.clone().and_then(|group| group.upgrade());
|
||||
|
||||
v_flex()
|
||||
.image_cache(retain_all("sidebar"))
|
||||
.size_full()
|
||||
@@ -916,7 +1174,10 @@ impl Render for Sidebar {
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(self.render_tabs(cx))
|
||||
.map(|this| match group {
|
||||
Some(group) => this.child(self.render_group(group, cx)),
|
||||
None => this.child(self.render_tabs(cx)),
|
||||
})
|
||||
.when(loading && logged_in, |this| {
|
||||
this.child(
|
||||
div()
|
||||
|
||||
Reference in New Issue
Block a user