update community sidebar
This commit is contained in:
+40
-13
@@ -7,11 +7,12 @@ use auto_update::AutoUpdater;
|
||||
use chat::{ChatEvent, ChatRegistry};
|
||||
use common::download_dir;
|
||||
use community::{CommunityEvent, CommunityRegistry};
|
||||
use community_ui::CommunityPanel;
|
||||
use device::{DeviceEvent, DeviceRegistry};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
Action, AnyElement, App, AppContext, Context, Entity, InteractiveElement, IntoElement,
|
||||
ParentElement, Render, SharedString, Styled, Subscription, Task, Window, div, px,
|
||||
ParentElement, Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, div, px,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::{PersonRegistry, shorten_pubkey};
|
||||
@@ -71,6 +72,8 @@ enum Command {
|
||||
pub struct Workspace {
|
||||
dock: Entity<DockArea>,
|
||||
title_bar_chrome: Rc<dock::TitleBarChrome>,
|
||||
/// The community panel currently docked, if any
|
||||
community_panel: Option<WeakEntity<CommunityPanel>>,
|
||||
/// Async tasks
|
||||
tasks: Vec<Task<Result<(), Error>>>,
|
||||
/// Event subscriptions
|
||||
@@ -219,17 +222,40 @@ impl Workspace {
|
||||
cx.subscribe_in(
|
||||
&communities,
|
||||
window,
|
||||
move |this, communities, event, window, cx| {
|
||||
if let CommunityEvent::Open(id) = event
|
||||
&& let Some(community) = communities.read(cx).community(id)
|
||||
{
|
||||
this.add_panel_to_dock(
|
||||
community_ui::init(community, window, cx),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
move |this, communities, event, window, cx| match event {
|
||||
CommunityEvent::Open(id) => {
|
||||
if let Some(community) = communities.read(cx).community(id) {
|
||||
let panel = community_ui::init(community, window, cx);
|
||||
|
||||
this.community_panel = Some(panel.downgrade());
|
||||
this.add_panel_to_dock(panel, DockPlacement::Center, window, cx);
|
||||
}
|
||||
}
|
||||
CommunityEvent::Close(_) => {
|
||||
let Some(panel) = this
|
||||
.community_panel
|
||||
.take()
|
||||
.and_then(|panel| panel.upgrade())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
this.dock.update(cx, |area, cx| {
|
||||
ui::dock::add_panel(
|
||||
area,
|
||||
PanelHandle::new(panel),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
ui::dock::focus_tab_panel(area, window, cx);
|
||||
|
||||
cx.defer_in(window, |_, window, cx| {
|
||||
window.dispatch_action(Box::new(ClosePanel), cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -255,13 +281,13 @@ impl Workspace {
|
||||
Self {
|
||||
dock,
|
||||
title_bar_chrome,
|
||||
community_panel: None,
|
||||
tasks: vec![],
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a panel to the dock, from anywhere that has the window but not the
|
||||
/// workspace.
|
||||
/// Add a panel to the dock, from anywhere that has the window but not the workspace.
|
||||
pub fn add_panel<P: Panel>(
|
||||
panel: Entity<P>,
|
||||
placement: DockPlacement,
|
||||
@@ -732,6 +758,7 @@ impl Render for Workspace {
|
||||
let modal_layer = Root::render_modal_layer(window, cx);
|
||||
let notification_layer = Root::render_notification_layer(window, cx);
|
||||
|
||||
// Render the title bar chrome
|
||||
self.title_bar_chrome.set_trailing(Self::titlebar_right);
|
||||
|
||||
div()
|
||||
|
||||
+358
-109
@@ -4,13 +4,15 @@ use std::rc::Rc;
|
||||
use auto_update::AutoUpdater;
|
||||
use chat::{ChatEvent, ChatRegistry, RoomKind};
|
||||
use common::TimestampExt;
|
||||
use community::{Community, CommunityEvent, CommunityRegistry};
|
||||
use community::{ChannelId, Community, CommunityEvent, CommunityRegistry};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||
UniformListScrollHandle, Window, div, px, retain_all, uniform_list,
|
||||
InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, ScrollHandle, SharedString,
|
||||
StatefulInteractiveElement, Styled, StyledImage, Subscription, UniformListScrollHandle,
|
||||
WeakEntity, Window, div, img, px, retain_all, uniform_list,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::PersonRegistry;
|
||||
use settings::AppSettings;
|
||||
use smallvec::{SmallVec, smallvec};
|
||||
@@ -42,10 +44,18 @@ pub(crate) use tree::{TreeRow, TreeRowKind};
|
||||
pub struct Sidebar {
|
||||
focus_handle: FocusHandle,
|
||||
scroll_handles: [UniformListScrollHandle; 3],
|
||||
/// Scroll state of the channel and member lists
|
||||
community_scroll: ScrollHandle,
|
||||
active_tab: SidebarTab,
|
||||
/// The community the sidebar is browsing, if any
|
||||
community: Option<WeakEntity<Community>>,
|
||||
/// Expanded state of the community's sections
|
||||
channels_open: bool,
|
||||
admins_open: bool,
|
||||
members_open: bool,
|
||||
/// Whether there are new chat requests
|
||||
new_requests: bool,
|
||||
_subscriptions: SmallVec<[Subscription; 3]>,
|
||||
_subscriptions: SmallVec<[Subscription; 4]>,
|
||||
}
|
||||
|
||||
impl Sidebar {
|
||||
@@ -78,6 +88,8 @@ impl Sidebar {
|
||||
|
||||
subscriptions.push(cx.observe(&nostr, |_this, _nostr, cx| cx.notify()));
|
||||
|
||||
subscriptions.push(cx.observe(&communities, |_this, _communities, cx| cx.notify()));
|
||||
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
scroll_handles: [
|
||||
@@ -85,7 +97,12 @@ impl Sidebar {
|
||||
UniformListScrollHandle::new(),
|
||||
UniformListScrollHandle::new(),
|
||||
],
|
||||
community_scroll: ScrollHandle::default(),
|
||||
active_tab: SidebarTab::Recents,
|
||||
community: None,
|
||||
channels_open: true,
|
||||
admins_open: true,
|
||||
members_open: true,
|
||||
new_requests: false,
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
@@ -117,110 +134,129 @@ impl Sidebar {
|
||||
registry.emit_community(&community, window, cx);
|
||||
});
|
||||
|
||||
self.community = Some(community.downgrade());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_user(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn close_community(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(community) = self.community.take() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Ok(id) = community.read_with(cx, |community, _cx| community.id()) {
|
||||
CommunityRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.emit_close(id, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_user(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let current_user = nostr.read(cx).current_user();
|
||||
|
||||
title_bar_drag_handlers(
|
||||
h_flex()
|
||||
.id("sidebar-user")
|
||||
.w_full()
|
||||
.h(TABBAR_HEIGHT)
|
||||
.flex_shrink_0()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.when(cfg!(target_os = "macos"), |this| {
|
||||
this.pl(px(TRAFFIC_LIGHT_PADDING))
|
||||
})
|
||||
.when_some(current_user.as_ref(), |this, public_key| {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
let profile = persons.read(cx).get(public_key, cx);
|
||||
let avatar = profile.avatar();
|
||||
let avatar_seed = profile.avatar_seed();
|
||||
let name = profile.name();
|
||||
let mut row = h_flex()
|
||||
.id("sidebar-user")
|
||||
.w_full()
|
||||
.h(TABBAR_HEIGHT)
|
||||
.flex_shrink_0()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.when(cfg!(target_os = "macos"), |this| {
|
||||
this.pl(px(TRAFFIC_LIGHT_PADDING))
|
||||
});
|
||||
|
||||
this.child(
|
||||
Button::new("current-user")
|
||||
.child(
|
||||
Avatar::new(avatar.clone())
|
||||
.seed(avatar_seed.clone())
|
||||
.xsmall(),
|
||||
)
|
||||
.small()
|
||||
.caret()
|
||||
.compact()
|
||||
.transparent()
|
||||
.dropdown_menu(move |this, _window, cx| {
|
||||
let avatar = avatar.clone();
|
||||
let avatar_seed = avatar_seed.clone();
|
||||
let name = name.clone();
|
||||
if let Some(public_key) = current_user.as_ref() {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
let profile = persons.read(cx).get(public_key, cx);
|
||||
let avatar = profile.avatar();
|
||||
let avatar_seed = profile.avatar_seed();
|
||||
let name = profile.name();
|
||||
|
||||
this.min_w(px(256.))
|
||||
.item(PopupMenuItem::element(move |_window, cx| {
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(
|
||||
Avatar::new(avatar.clone())
|
||||
.seed(avatar_seed.clone())
|
||||
.xsmall(),
|
||||
)
|
||||
.child(name.clone())
|
||||
}))
|
||||
.separator()
|
||||
.menu_with_icon(
|
||||
"Inbox",
|
||||
IconName::Inbox,
|
||||
Box::new(Command::ShowInbox),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Search",
|
||||
IconName::Search,
|
||||
Box::new(Command::ShowSearch),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Profile",
|
||||
IconName::Profile,
|
||||
Box::new(Command::ShowProfile),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Contact List",
|
||||
IconName::Book,
|
||||
Box::new(Command::ShowContactList),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Backup",
|
||||
IconName::UserKey,
|
||||
Box::new(Command::ShowBackup),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Themes",
|
||||
IconName::Sun,
|
||||
Box::new(Command::ToggleTheme),
|
||||
)
|
||||
.when(AutoUpdater::is_available(cx), |this| {
|
||||
this.separator().menu_with_icon(
|
||||
"Check for Updates",
|
||||
IconName::Device,
|
||||
Box::new(Command::Update),
|
||||
)
|
||||
})
|
||||
.menu_with_icon(
|
||||
"Settings",
|
||||
IconName::Settings,
|
||||
Box::new(Command::ShowSettings),
|
||||
)
|
||||
}),
|
||||
row = row.child(
|
||||
Button::new("current-user")
|
||||
.child(
|
||||
Avatar::new(avatar.clone())
|
||||
.seed(avatar_seed.clone())
|
||||
.xsmall(),
|
||||
)
|
||||
}),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.small()
|
||||
.caret()
|
||||
.compact()
|
||||
.transparent()
|
||||
.dropdown_menu(move |this, _window, cx| {
|
||||
let avatar = avatar.clone();
|
||||
let avatar_seed = avatar_seed.clone();
|
||||
let name = name.clone();
|
||||
|
||||
this.min_w(px(256.))
|
||||
.item(PopupMenuItem::element(move |_window, cx| {
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(
|
||||
Avatar::new(avatar.clone())
|
||||
.seed(avatar_seed.clone())
|
||||
.xsmall(),
|
||||
)
|
||||
.child(name.clone())
|
||||
}))
|
||||
.separator()
|
||||
.menu_with_icon("Inbox", IconName::Inbox, Box::new(Command::ShowInbox))
|
||||
.menu_with_icon(
|
||||
"Search",
|
||||
IconName::Search,
|
||||
Box::new(Command::ShowSearch),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Profile",
|
||||
IconName::Profile,
|
||||
Box::new(Command::ShowProfile),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Contact List",
|
||||
IconName::Book,
|
||||
Box::new(Command::ShowContactList),
|
||||
)
|
||||
.menu_with_icon(
|
||||
"Backup",
|
||||
IconName::UserKey,
|
||||
Box::new(Command::ShowBackup),
|
||||
)
|
||||
.menu_with_icon("Themes", IconName::Sun, Box::new(Command::ToggleTheme))
|
||||
.when(AutoUpdater::is_available(cx), |this| {
|
||||
this.separator().menu_with_icon(
|
||||
"Check for Updates",
|
||||
IconName::Device,
|
||||
Box::new(Command::Update),
|
||||
)
|
||||
})
|
||||
.menu_with_icon(
|
||||
"Settings",
|
||||
IconName::Settings,
|
||||
Box::new(Command::ShowSettings),
|
||||
)
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if self.community.is_some() {
|
||||
row = row.child(div().flex_1()).child(
|
||||
Button::new("sidebar-back")
|
||||
.icon(IconName::ArrowLeft)
|
||||
.tooltip("Back")
|
||||
.ghost()
|
||||
.small()
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.close_community(window, cx);
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
title_bar_drag_handlers(row, window, cx).into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,10 +495,12 @@ impl Focusable for Sidebar {
|
||||
|
||||
impl Render for Sidebar {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let (logged_in, ready) = {
|
||||
let nostr = nostr.read(cx);
|
||||
(nostr.current_user().is_some(), nostr.ready())
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
(
|
||||
nostr.read(cx).current_user().is_some(),
|
||||
nostr.read(cx).ready(),
|
||||
)
|
||||
};
|
||||
|
||||
if !logged_in {
|
||||
@@ -474,17 +512,17 @@ impl Render for Sidebar {
|
||||
.border_color(cx.theme().border_variant)
|
||||
.into_any_element();
|
||||
}
|
||||
|
||||
return onboarding::render(window, cx).into_any_element();
|
||||
}
|
||||
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let loading = chat.read(cx).loading();
|
||||
let community = self
|
||||
.community
|
||||
.clone()
|
||||
.and_then(|community| community.upgrade());
|
||||
|
||||
let sidebar = cx.entity().downgrade();
|
||||
let active_tab = self.active_tab;
|
||||
let rows = Rc::new(rows_for(active_tab, cx));
|
||||
let scroll_handle = &self.scroll_handles[active_tab.index()];
|
||||
if community.is_none() {
|
||||
self.community = None;
|
||||
}
|
||||
|
||||
v_flex()
|
||||
.image_cache(retain_all("sidebar"))
|
||||
@@ -495,6 +533,29 @@ impl Render for Sidebar {
|
||||
.border_r_1()
|
||||
.border_color(cx.theme().border_variant)
|
||||
.child(self.render_user(window, cx))
|
||||
.map(|this| match community {
|
||||
Some(community) => this.child(self.render_community(community, cx)),
|
||||
None => this.child(self.render_tabs(cx)),
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl Sidebar {
|
||||
fn render_tabs(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let loading = chat.read(cx).loading();
|
||||
|
||||
let sidebar = cx.entity().downgrade();
|
||||
let active_tab = self.active_tab;
|
||||
let rows = Rc::new(rows_for(active_tab, cx));
|
||||
let scroll_handle = &self.scroll_handles[active_tab.index()];
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.gap_1()
|
||||
.when(active_tab.chat(), |this| {
|
||||
this.child(
|
||||
v_flex()
|
||||
@@ -629,4 +690,192 @@ impl Render for Sidebar {
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_community(
|
||||
&mut self,
|
||||
community: Entity<Community>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let (channels, active, admins, members, banner) = {
|
||||
let community = community.read(cx);
|
||||
let owner = community.state().owner;
|
||||
let mut admins = Vec::new();
|
||||
let mut members = Vec::new();
|
||||
|
||||
for public_key in community.members() {
|
||||
if community.control().roles.is_staff(public_key, &owner) {
|
||||
admins.push(*public_key);
|
||||
} else {
|
||||
members.push(*public_key);
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
community
|
||||
.channels()
|
||||
.iter()
|
||||
.map(|channel| (channel.id, channel.name.clone(), channel.private))
|
||||
.collect::<Vec<_>>(),
|
||||
community.active_channel(),
|
||||
admins,
|
||||
members,
|
||||
community.banner(),
|
||||
)
|
||||
};
|
||||
|
||||
let sections = v_flex()
|
||||
.id("community-sections")
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.w_full()
|
||||
.px_2()
|
||||
.pb_2()
|
||||
.track_scroll(&self.community_scroll)
|
||||
.overflow_y_scroll()
|
||||
.child(section_row(
|
||||
"channels",
|
||||
"Channels",
|
||||
channels.len(),
|
||||
self.channels_open,
|
||||
|sidebar| sidebar.channels_open = !sidebar.channels_open,
|
||||
cx,
|
||||
))
|
||||
.when(self.channels_open, |this| {
|
||||
this.children(channels.into_iter().map(|(id, name, private)| {
|
||||
channel_row(id, name, private, active == Some(id), &community, cx)
|
||||
}))
|
||||
})
|
||||
.child(section_row(
|
||||
"admins",
|
||||
"Admins",
|
||||
admins.len(),
|
||||
self.admins_open,
|
||||
|sidebar| sidebar.admins_open = !sidebar.admins_open,
|
||||
cx,
|
||||
))
|
||||
.when(self.admins_open, |this| {
|
||||
this.children(
|
||||
admins
|
||||
.iter()
|
||||
.map(|public_key| member_row("community-admin", *public_key, cx)),
|
||||
)
|
||||
})
|
||||
.child(section_row(
|
||||
"members",
|
||||
"Members",
|
||||
members.len(),
|
||||
self.members_open,
|
||||
|sidebar| sidebar.members_open = !sidebar.members_open,
|
||||
cx,
|
||||
))
|
||||
.when(self.members_open, |this| {
|
||||
this.children(
|
||||
members
|
||||
.iter()
|
||||
.map(|public_key| member_row("member", *public_key, cx)),
|
||||
)
|
||||
});
|
||||
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.when_some(banner, |this, banner| {
|
||||
this.child(
|
||||
div().px_2().child(
|
||||
img(banner)
|
||||
.w_full()
|
||||
.h(px(80.))
|
||||
.rounded(cx.theme().radius)
|
||||
.object_fit(ObjectFit::Cover),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(sections)
|
||||
.child(Scrollbar::vertical(&self.community_scroll))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
fn section_row(
|
||||
id: &'static str,
|
||||
label: &'static str,
|
||||
count: usize,
|
||||
open: bool,
|
||||
toggle: impl Fn(&mut Sidebar) + 'static,
|
||||
cx: &mut Context<Sidebar>,
|
||||
) -> AnyElement {
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.child(
|
||||
TreeRow::new(ElementId::Name(id.into()), TreeRowKind::Section, label)
|
||||
.icon(if open {
|
||||
IconName::CaretDown
|
||||
} else {
|
||||
IconName::CaretRight
|
||||
})
|
||||
.count(count)
|
||||
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||
toggle(this);
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn channel_row(
|
||||
id: ChannelId,
|
||||
name: String,
|
||||
private: bool,
|
||||
selected: bool,
|
||||
community: &Entity<Community>,
|
||||
cx: &mut Context<Sidebar>,
|
||||
) -> AnyElement {
|
||||
let community = community.clone();
|
||||
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.child(
|
||||
TreeRow::new(
|
||||
ElementId::Name(SharedString::from(format!(
|
||||
"community-channel-{}",
|
||||
id.to_hex()
|
||||
))),
|
||||
TreeRowKind::Room,
|
||||
name,
|
||||
)
|
||||
.icon(if private {
|
||||
IconName::Lock
|
||||
} else {
|
||||
IconName::Message
|
||||
})
|
||||
.on_click(cx.listener(move |_this, _event, _window, cx| {
|
||||
community.update(cx, |community, cx| community.set_active_channel(id, cx));
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.when(selected, |this| this.bg(cx.theme().ghost_element_active))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn member_row(prefix: &str, public_key: PublicKey, cx: &App) -> AnyElement {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
let person = persons.read(cx).get(&public_key, cx);
|
||||
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.child(
|
||||
TreeRow::new(
|
||||
ElementId::Name(SharedString::from(format!(
|
||||
"{prefix}-{}",
|
||||
public_key.to_hex()
|
||||
))),
|
||||
TreeRowKind::Room,
|
||||
person.name(),
|
||||
)
|
||||
.avatar(person.avatar_seed())
|
||||
.picture(person.avatar()),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user