update sidebar
This commit is contained in:
+329
-410
@@ -2,19 +2,18 @@ use std::ops::Range;
|
||||
use std::rc::Rc;
|
||||
|
||||
use auto_update::AutoUpdater;
|
||||
use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
|
||||
use chat::{ChatEvent, ChatRegistry, RoomKind};
|
||||
use common::TimestampExt;
|
||||
use community::{ChannelId, Community, CommunityEvent, CommunityRegistry};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, Div, ElementId, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, ScrollHandle, SharedString,
|
||||
Stateful, StatefulInteractiveElement, Styled, StyledImage, Subscription,
|
||||
UniformListScrollHandle, WeakEntity, Window, div, img, px, retain_all, uniform_list,
|
||||
InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, SharedString, Stateful,
|
||||
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};
|
||||
use state::NostrRegistry;
|
||||
use theme::{ActiveTheme, TABBAR_HEIGHT};
|
||||
@@ -26,9 +25,11 @@ use ui::menu::{DropdownMenu, PopupMenuItem};
|
||||
use ui::nav_item::NavItem;
|
||||
use ui::notification::Notification;
|
||||
use ui::scroll::Scrollbar;
|
||||
use ui::tab::Tab;
|
||||
use ui::tab::tab_bar::TabBar;
|
||||
use ui::{
|
||||
Icon, IconName, Sizable, StyledExt, TRAFFIC_LIGHT_PADDING, WindowExtension, h_flex,
|
||||
title_bar_drag_handlers, v_flex,
|
||||
Disableable, Icon, IconName, Sizable, StyledExt, TRAFFIC_LIGHT_PADDING, WindowExtension,
|
||||
h_flex, title_bar_drag_handlers, v_flex,
|
||||
};
|
||||
|
||||
use crate::Command;
|
||||
@@ -37,15 +38,15 @@ use crate::dialogs::import;
|
||||
mod tab;
|
||||
mod tree;
|
||||
|
||||
use tab::{SidebarTab, TabBar};
|
||||
use tree::SidebarRow;
|
||||
use tab::SidebarTab;
|
||||
use tree::{CommunityRow, CommunitySection, SidebarRow};
|
||||
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,
|
||||
scroll_handles: [UniformListScrollHandle; 2],
|
||||
/// Scroll state of the community's channel and member lists
|
||||
community_scroll: UniformListScrollHandle,
|
||||
/// The dock the sidebar opens its panels in
|
||||
dock: WeakEntity<DockArea>,
|
||||
active_tab: SidebarTab,
|
||||
@@ -89,11 +90,10 @@ impl Sidebar {
|
||||
scroll_handles: [
|
||||
UniformListScrollHandle::new(),
|
||||
UniformListScrollHandle::new(),
|
||||
UniformListScrollHandle::new(),
|
||||
],
|
||||
community_scroll: ScrollHandle::default(),
|
||||
community_scroll: UniformListScrollHandle::new(),
|
||||
dock,
|
||||
active_tab: SidebarTab::Recents,
|
||||
active_tab: SidebarTab::Inbox,
|
||||
community: None,
|
||||
channels_open: true,
|
||||
admins_open: true,
|
||||
@@ -111,42 +111,6 @@ impl Sidebar {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn open_community(
|
||||
&mut self,
|
||||
community: Entity<Community>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let id = community.read(cx).id().to_hex();
|
||||
let settings = AppSettings::global(cx);
|
||||
|
||||
settings.update(cx, |settings, cx| {
|
||||
settings.record_recent_community(id, cx);
|
||||
});
|
||||
|
||||
self.community = Some(community.downgrade());
|
||||
|
||||
ui::dock::add_panel_to(
|
||||
&self.dock,
|
||||
PanelHandle::new(community_ui::init(community, window, cx)),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn open_room(&mut self, room: Entity<Room>, window: &mut Window, cx: &mut Context<Self>) {
|
||||
ui::dock::add_panel_to(
|
||||
&self.dock,
|
||||
PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
/// Leave the community view, returning the sidebar to its tab list.
|
||||
fn reset_community(&mut self, cx: &mut Context<Self>) {
|
||||
if self.community.take().is_none() {
|
||||
@@ -155,9 +119,12 @@ impl Sidebar {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_user(&self, cx: &mut Context<Self>) -> Stateful<Div> {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let current_user = nostr.read(cx).current_user();
|
||||
fn render_user(&self, current_user: &PublicKey, cx: &mut Context<Self>) -> Stateful<Div> {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
let profile = persons.read(cx).get(current_user, cx);
|
||||
let avatar = profile.avatar();
|
||||
let avatar_seed = profile.avatar_seed();
|
||||
let name = profile.name();
|
||||
|
||||
h_flex()
|
||||
.id("sidebar-user")
|
||||
@@ -170,88 +137,66 @@ impl Sidebar {
|
||||
.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();
|
||||
.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();
|
||||
|
||||
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();
|
||||
|
||||
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),
|
||||
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(),
|
||||
)
|
||||
})
|
||||
.menu_with_icon(
|
||||
"Settings",
|
||||
IconName::Settings,
|
||||
Box::new(Command::ShowSettings),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.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))
|
||||
.separator()
|
||||
.menu_with_icon(
|
||||
"Settings",
|
||||
IconName::Settings,
|
||||
Box::new(Command::ShowSettings),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.when_some(AutoUpdater::try_global(cx), |this, updater| {
|
||||
this.child(self.render_updater(updater, cx))
|
||||
@@ -272,12 +217,21 @@ impl Sidebar {
|
||||
|
||||
fn render_updater(&self, updater: Entity<AutoUpdater>, cx: &mut App) -> AnyElement {
|
||||
let status = updater.read(cx).status();
|
||||
let up_to_date = updater.read(cx).up_to_date();
|
||||
let staged = updater.read(cx).staged();
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.text_xs()
|
||||
.child(status)
|
||||
.when(!up_to_date, |this| {
|
||||
this.child(
|
||||
Button::new("update-status")
|
||||
.icon(IconName::ArrowDownCircle)
|
||||
.tooltip(status)
|
||||
.small()
|
||||
.warning()
|
||||
.disabled(true),
|
||||
)
|
||||
})
|
||||
.when(staged, |this| {
|
||||
this.child(
|
||||
Button::new("restart-to-update")
|
||||
@@ -296,9 +250,6 @@ 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(self.rows_for(active_tab, cx));
|
||||
@@ -308,90 +259,95 @@ impl Sidebar {
|
||||
.size_full()
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.gap_1()
|
||||
.when(active_tab.chat(), |this| {
|
||||
this.child(
|
||||
.gap_2()
|
||||
.child(
|
||||
div().px_2().child(
|
||||
TabBar::new("sidebar-tabs")
|
||||
.segmented(true)
|
||||
.selected_index(active_tab.index())
|
||||
.child(Tab::new().label(SidebarTab::Inbox.label()))
|
||||
.child(Tab::new().label(SidebarTab::Communities.label()))
|
||||
.on_click({
|
||||
let sidebar = sidebar.clone();
|
||||
move |index, _window, cx| {
|
||||
let Some(tab) = SidebarTab::ALL.get(*index).copied() else {
|
||||
return;
|
||||
};
|
||||
if let Err(error) =
|
||||
sidebar.update(cx, |this, cx| this.select_tab(tab, cx))
|
||||
{
|
||||
log::error!("Failed to switch sidebar tab: {error}");
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.map(|this| match active_tab {
|
||||
SidebarTab::Inbox => this.child(
|
||||
v_flex()
|
||||
.px_2()
|
||||
.gap_1()
|
||||
.child(
|
||||
NavItem::new(
|
||||
"nav-contacts",
|
||||
"Contacts",
|
||||
Icon::new(IconName::Book).small(),
|
||||
)
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(Box::new(Command::ShowContactList), cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
NavItem::new(
|
||||
"nav-requests",
|
||||
"Requests",
|
||||
Icon::new(IconName::Invite).small(),
|
||||
)
|
||||
.when(self.new_requests, |this| {
|
||||
this.suffix(div().size_1().rounded_full().bg(cx.theme().cursor))
|
||||
})
|
||||
.on_click({
|
||||
let sidebar = sidebar.clone();
|
||||
move |_event, window, cx| {
|
||||
if let Err(error) = sidebar.update(cx, |this, cx| {
|
||||
this.new_requests = false;
|
||||
cx.notify();
|
||||
}) {
|
||||
log::error!("Failed to clear new requests: {error}");
|
||||
}
|
||||
window.dispatch_action(Box::new(Command::ShowRequests), cx);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
NavItem::new(
|
||||
"nav-new-chat",
|
||||
"New chat",
|
||||
Icon::new(IconName::Plus).small(),
|
||||
"new-chat",
|
||||
"New Chat",
|
||||
Icon::new(IconName::Message).small(),
|
||||
)
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(Box::new(Command::NewChat), cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
NavItem::new("reqs", "Requests", Icon::new(IconName::Invite).small())
|
||||
.when(self.new_requests, |this| {
|
||||
this.suffix(div().size_1().rounded_full().bg(cx.theme().cursor))
|
||||
})
|
||||
.on_click({
|
||||
let sidebar = sidebar.clone();
|
||||
move |_event, window, cx| {
|
||||
if let Err(error) = sidebar.update(cx, |this, cx| {
|
||||
this.new_requests = false;
|
||||
cx.notify();
|
||||
}) {
|
||||
log::error!("Failed to clear new requests: {error}");
|
||||
}
|
||||
window.dispatch_action(Box::new(Command::ShowRequests), cx);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
NavItem::new("contacts", "Contacts", Icon::new(IconName::Book).small())
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(Box::new(Command::ShowContactList), cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(active_tab.community(), |this| {
|
||||
this.child(
|
||||
),
|
||||
SidebarTab::Communities => this.child(
|
||||
v_flex()
|
||||
.px_2()
|
||||
.gap_1()
|
||||
.child(
|
||||
NavItem::new(
|
||||
"nav-browse",
|
||||
"Browse",
|
||||
Icon::new(IconName::Compass).small(),
|
||||
"new-community",
|
||||
"New Community",
|
||||
Icon::new(IconName::Group).small(),
|
||||
)
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(Box::new(Command::ShowBrowse), cx)
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(Box::new(Command::NewCommunity), cx)
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
NavItem::new(
|
||||
"nav-new-community",
|
||||
"New community",
|
||||
Icon::new(IconName::Plus).small(),
|
||||
)
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(Box::new(Command::NewCommunity), cx)
|
||||
}),
|
||||
NavItem::new("browse", "Browse", Icon::new(IconName::Compass).small())
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(Box::new(Command::ShowBrowse), cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
),
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.size_full()
|
||||
.flex_1()
|
||||
div()
|
||||
.min_h_0()
|
||||
.gap_1()
|
||||
.pb_12()
|
||||
.flex_1()
|
||||
.child(
|
||||
uniform_list(
|
||||
active_tab.list_id(),
|
||||
@@ -401,46 +357,11 @@ impl Sidebar {
|
||||
}),
|
||||
)
|
||||
.track_scroll(scroll_handle)
|
||||
.flex_1()
|
||||
.h_full()
|
||||
.px_2(),
|
||||
)
|
||||
.child(Scrollbar::vertical(scroll_handle)),
|
||||
)
|
||||
.child(TabBar::new(active_tab).on_select({
|
||||
let sidebar = sidebar.clone();
|
||||
move |tab, _window, cx| {
|
||||
if let Err(error) = sidebar.update(cx, |this, cx| this.select_tab(tab, cx)) {
|
||||
log::error!("Failed to switch sidebar tab: {error}");
|
||||
}
|
||||
}
|
||||
}))
|
||||
.when(loading, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.bottom_16()
|
||||
.left_0()
|
||||
.h_9()
|
||||
.w_full()
|
||||
.px_8()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.h_9()
|
||||
.justify_center()
|
||||
.bg(cx.theme().background.opacity(0.85))
|
||||
.when(cx.theme().shadow, |this| this.shadow_md())
|
||||
.rounded_full()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(Indicator::new().small().color(cx.theme().icon_accent))
|
||||
.child("Getting messages..."),
|
||||
),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -449,7 +370,7 @@ impl Sidebar {
|
||||
community: Entity<Community>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let (channels, active, admins, members, banner) = {
|
||||
let (banner, rows) = {
|
||||
let community = community.read(cx);
|
||||
let owner = community.state().owner;
|
||||
let mut admins = Vec::new();
|
||||
@@ -463,71 +384,45 @@ impl Sidebar {
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
community
|
||||
.channels()
|
||||
.iter()
|
||||
.map(|channel| (channel.id, channel.name.clone(), channel.private))
|
||||
.collect::<Vec<_>>(),
|
||||
community.active_channel(),
|
||||
admins,
|
||||
members,
|
||||
community.banner(),
|
||||
)
|
||||
let active = community.active_channel();
|
||||
let banner = community.banner();
|
||||
|
||||
let mut rows = vec![CommunityRow::Section(CommunitySection::Channels)];
|
||||
if self.channels_open {
|
||||
rows.extend(
|
||||
community
|
||||
.channels()
|
||||
.iter()
|
||||
.map(|channel| CommunityRow::Channel {
|
||||
id: channel.id,
|
||||
name: channel.name.clone().into(),
|
||||
private: channel.private,
|
||||
selected: active == Some(channel.id),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
rows.push(CommunityRow::Section(CommunitySection::Admins));
|
||||
if self.admins_open {
|
||||
rows.extend(admins.into_iter().map(|public_key| CommunityRow::Member {
|
||||
prefix: "community-admin",
|
||||
public_key,
|
||||
}));
|
||||
}
|
||||
|
||||
rows.push(CommunityRow::Section(CommunitySection::Members));
|
||||
if self.members_open {
|
||||
rows.extend(members.into_iter().map(|public_key| CommunityRow::Member {
|
||||
prefix: "member",
|
||||
public_key,
|
||||
}));
|
||||
}
|
||||
|
||||
(banner, rows)
|
||||
};
|
||||
|
||||
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(self.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)| {
|
||||
self.channel_row(id, name, private, active == Some(id), &community, cx)
|
||||
}))
|
||||
})
|
||||
.child(self.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| self.member_row("community-admin", *public_key, cx)),
|
||||
)
|
||||
})
|
||||
.child(self.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| self.member_row("member", *public_key, cx)),
|
||||
)
|
||||
});
|
||||
let rows = Rc::new(rows);
|
||||
let scroll_handle = &self.community_scroll;
|
||||
|
||||
v_flex()
|
||||
.flex_1()
|
||||
@@ -539,110 +434,94 @@ impl Sidebar {
|
||||
div().px_2().child(
|
||||
img(banner)
|
||||
.w_full()
|
||||
.h(px(80.))
|
||||
.rounded(cx.theme().radius)
|
||||
.h_20()
|
||||
.rounded(cx.theme().radius_lg)
|
||||
.object_fit(ObjectFit::Cover),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(sections)
|
||||
.child(Scrollbar::vertical(&self.community_scroll))
|
||||
.child(
|
||||
div()
|
||||
.min_h_0()
|
||||
.flex_1()
|
||||
.child(
|
||||
uniform_list(
|
||||
"community-rows",
|
||||
rows.len(),
|
||||
cx.processor(move |this, range, _window, cx| {
|
||||
this.render_community_rows(range, rows.as_slice(), &community, cx)
|
||||
}),
|
||||
)
|
||||
.track_scroll(scroll_handle)
|
||||
.h_full()
|
||||
.px_2(),
|
||||
)
|
||||
.child(Scrollbar::vertical(scroll_handle)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn recent_communities(&self, cx: &App) -> Vec<Entity<Community>> {
|
||||
const LIMIT: usize = 3;
|
||||
|
||||
let registry = CommunityRegistry::global(cx);
|
||||
let communities = registry.read(cx).communities();
|
||||
let recent = AppSettings::get_recent_communities(cx);
|
||||
|
||||
let mut rows: Vec<Entity<Community>> = recent
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
communities
|
||||
.iter()
|
||||
.find(|community| community.read(cx).id().to_hex() == *id)
|
||||
fn render_community_rows(
|
||||
&self,
|
||||
range: Range<usize>,
|
||||
rows: &[CommunityRow],
|
||||
community: &Entity<Community>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<AnyElement> {
|
||||
rows.get(range)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|row| match row {
|
||||
CommunityRow::Section(CommunitySection::Channels) => self.section_row(
|
||||
"channels",
|
||||
"Channels",
|
||||
self.channels_open,
|
||||
|sidebar| sidebar.channels_open = !sidebar.channels_open,
|
||||
cx,
|
||||
),
|
||||
CommunityRow::Section(CommunitySection::Admins) => self.section_row(
|
||||
"admins",
|
||||
"Admins",
|
||||
self.admins_open,
|
||||
|sidebar| sidebar.admins_open = !sidebar.admins_open,
|
||||
cx,
|
||||
),
|
||||
CommunityRow::Section(CommunitySection::Members) => self.section_row(
|
||||
"members",
|
||||
"Members",
|
||||
self.members_open,
|
||||
|sidebar| sidebar.members_open = !sidebar.members_open,
|
||||
cx,
|
||||
),
|
||||
CommunityRow::Channel {
|
||||
id,
|
||||
name,
|
||||
private,
|
||||
selected,
|
||||
} => self.channel_row(*id, name.clone(), *private, *selected, community, cx),
|
||||
CommunityRow::Member { prefix, public_key } => {
|
||||
self.member_row(prefix, *public_key, cx)
|
||||
}
|
||||
})
|
||||
.take(LIMIT)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
if rows.is_empty() {
|
||||
rows = communities.iter().take(LIMIT).cloned().collect();
|
||||
}
|
||||
|
||||
rows
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn rows_for(&self, tab: SidebarTab, cx: &App) -> Vec<SidebarRow> {
|
||||
match tab {
|
||||
SidebarTab::Recents => {
|
||||
SidebarTab::Inbox => {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let rooms = chat.read(cx).rooms(&RoomKind::Ongoing, cx);
|
||||
let registry = CommunityRegistry::global(cx);
|
||||
let community_count = registry.read(cx).communities().len();
|
||||
let communities = self.recent_communities(cx);
|
||||
|
||||
if communities.is_empty() && rooms.is_empty() {
|
||||
return vec![SidebarRow::Hint {
|
||||
text: "Nothing recent yet".into(),
|
||||
}];
|
||||
}
|
||||
|
||||
let mut rows = Vec::new();
|
||||
|
||||
if !communities.is_empty() {
|
||||
rows.push(SidebarRow::Section {
|
||||
label: "Communities".into(),
|
||||
count: community_count,
|
||||
});
|
||||
rows.extend(
|
||||
communities
|
||||
.into_iter()
|
||||
.map(|community| SidebarRow::Community { community }),
|
||||
);
|
||||
rows.push(SidebarRow::Action {
|
||||
label: "Show all communities".into(),
|
||||
tab: SidebarTab::Communities,
|
||||
});
|
||||
}
|
||||
|
||||
if !rooms.is_empty() {
|
||||
rows.push(SidebarRow::Section {
|
||||
label: "Chats".into(),
|
||||
count: rooms.len(),
|
||||
});
|
||||
rows.extend(
|
||||
rooms
|
||||
.into_iter()
|
||||
.take(5)
|
||||
.map(|room| SidebarRow::Room { room }),
|
||||
);
|
||||
rows.push(SidebarRow::Action {
|
||||
label: "Show all chats".into(),
|
||||
tab: SidebarTab::Chats,
|
||||
});
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
SidebarTab::Chats => {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let chat = chat.read(cx);
|
||||
let messages = chat.rooms(&RoomKind::Ongoing, cx);
|
||||
|
||||
let mut rows = vec![SidebarRow::Section {
|
||||
label: "Chats".into(),
|
||||
count: messages.len(),
|
||||
}];
|
||||
|
||||
if messages.is_empty() {
|
||||
if rooms.is_empty() {
|
||||
rows.push(SidebarRow::Hint {
|
||||
text: "No conversations yet".into(),
|
||||
});
|
||||
} else {
|
||||
rows.extend(messages.into_iter().map(|room| SidebarRow::Room { room }));
|
||||
rows.extend(rooms.into_iter().map(|room| SidebarRow::Room { room }));
|
||||
}
|
||||
|
||||
rows
|
||||
@@ -653,7 +532,6 @@ impl Sidebar {
|
||||
|
||||
let mut rows = vec![SidebarRow::Section {
|
||||
label: "Communities".into(),
|
||||
count: communities.len(),
|
||||
}];
|
||||
|
||||
if communities.is_empty() {
|
||||
@@ -688,23 +566,19 @@ impl Sidebar {
|
||||
let index = range.start + offset;
|
||||
|
||||
match row {
|
||||
SidebarRow::Section { label, count } => TreeRow::new(
|
||||
SidebarRow::Section { label } => TreeRow::new(
|
||||
ElementId::NamedInteger("tree-row".into(), index as u64),
|
||||
TreeRowKind::Section,
|
||||
label.clone(),
|
||||
)
|
||||
.count(*count)
|
||||
.into_any_element(),
|
||||
SidebarRow::Room { room } => {
|
||||
let name = room.read(cx).display_name(cx);
|
||||
let picture = room.read(cx).display_image(cx);
|
||||
let seed = room.read(cx).display_image_seed(cx);
|
||||
let created_at = room.read(cx).created_at.to_ago();
|
||||
let room_clone = room.clone();
|
||||
|
||||
let handler = cx.listener(move |this, _event, window, cx| {
|
||||
this.open_room(room_clone.clone(), window, cx);
|
||||
});
|
||||
let dock = self.dock.clone();
|
||||
let room = room.clone();
|
||||
|
||||
TreeRow::new(
|
||||
ElementId::NamedInteger("tree-row".into(), index as u64),
|
||||
@@ -714,13 +588,25 @@ impl Sidebar {
|
||||
.avatar(seed)
|
||||
.picture(picture)
|
||||
.created_at(created_at)
|
||||
.on_click(handler)
|
||||
// Run in an app context: docking a new panel reads every
|
||||
// docked panel's id, which would panic inside a sidebar update.
|
||||
.on_click(move |_event, window, cx| {
|
||||
ui::dock::add_panel_to(
|
||||
&dock,
|
||||
PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarRow::Community { community } => {
|
||||
let name = community.read(cx).name();
|
||||
let seed = community.read(cx).id().to_hex();
|
||||
let picture = community.read(cx).icon();
|
||||
let dock = self.dock.clone();
|
||||
let sidebar = cx.entity().downgrade();
|
||||
let community = community.clone();
|
||||
|
||||
TreeRow::new(
|
||||
@@ -730,23 +616,22 @@ impl Sidebar {
|
||||
)
|
||||
.avatar(seed)
|
||||
.picture(picture)
|
||||
.on_click(cx.listener(move |this, _event, window, cx| {
|
||||
this.open_community(community.clone(), window, cx);
|
||||
}))
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarRow::Action { label, tab } => {
|
||||
let tab = *tab;
|
||||
.on_click(move |_event, window, cx| {
|
||||
ui::dock::add_panel_to(
|
||||
&dock,
|
||||
PanelHandle::new(community_ui::init(community.clone(), window, cx)),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
||||
TreeRow::new(
|
||||
ElementId::NamedInteger("tree-row".into(), index as u64),
|
||||
TreeRowKind::Action,
|
||||
label.clone(),
|
||||
)
|
||||
.icon(IconName::ArrowRight)
|
||||
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||
this.select_tab(tab, cx);
|
||||
}))
|
||||
if let Err(error) = sidebar.update(cx, |this, cx| {
|
||||
this.community = Some(community.downgrade());
|
||||
cx.notify();
|
||||
}) {
|
||||
log::error!("Failed to show community in sidebar: {error}");
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarRow::Hint { text } => TreeRow::new(
|
||||
@@ -764,7 +649,6 @@ impl Sidebar {
|
||||
&self,
|
||||
id: &'static str,
|
||||
label: &'static str,
|
||||
count: usize,
|
||||
open: bool,
|
||||
toggle: impl Fn(&mut Sidebar) + 'static,
|
||||
cx: &mut Context<Sidebar>,
|
||||
@@ -778,7 +662,6 @@ impl Sidebar {
|
||||
} else {
|
||||
IconName::CaretRight
|
||||
})
|
||||
.count(count)
|
||||
.on_click(cx.listener(move |this, _event, _window, cx| {
|
||||
toggle(this);
|
||||
cx.notify();
|
||||
@@ -790,7 +673,7 @@ impl Sidebar {
|
||||
fn channel_row(
|
||||
&self,
|
||||
id: ChannelId,
|
||||
name: String,
|
||||
name: SharedString,
|
||||
private: bool,
|
||||
selected: bool,
|
||||
community: &Entity<Community>,
|
||||
@@ -875,7 +758,11 @@ 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 = { nostr.read(cx).current_user().is_some() };
|
||||
let current_user = nostr.read(cx).current_user();
|
||||
let logged_in = current_user.is_some();
|
||||
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let loading = chat.read(cx).loading();
|
||||
|
||||
let community = self
|
||||
.community
|
||||
@@ -890,6 +777,13 @@ impl Render for Sidebar {
|
||||
.bg(cx.theme().surface_background)
|
||||
.border_r_1()
|
||||
.border_color(cx.theme().border_variant)
|
||||
.when_some(current_user.as_ref(), |this, current_user| {
|
||||
this.child(title_bar_drag_handlers(
|
||||
self.render_user(current_user, cx),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
})
|
||||
.when(!logged_in, |this| {
|
||||
this.relative()
|
||||
.child(title_bar_drag_handlers(
|
||||
@@ -930,11 +824,36 @@ impl Render for Sidebar {
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(title_bar_drag_handlers(self.render_user(cx), window, cx))
|
||||
.map(|this| match community {
|
||||
Some(community) => this.child(self.render_community(community, cx)),
|
||||
None => this.child(self.render_tabs(cx)),
|
||||
})
|
||||
.when(loading && logged_in, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.bottom_4()
|
||||
.left_0()
|
||||
.h_9()
|
||||
.w_full()
|
||||
.px_8()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.h_9()
|
||||
.justify_center()
|
||||
.bg(cx.theme().background.opacity(0.85))
|
||||
.when(cx.theme().shadow, |this| this.shadow_md())
|
||||
.rounded_full()
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child(Indicator::new().small().color(cx.theme().icon_accent))
|
||||
.child("Getting messages..."),
|
||||
),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,121 +1,30 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{App, InteractiveElement, IntoElement, ParentElement, RenderOnce, Styled, Window, div};
|
||||
use theme::ActiveTheme;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::{IconName, Selectable, h_flex};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SidebarTab {
|
||||
Recents,
|
||||
Chats,
|
||||
Inbox,
|
||||
Communities,
|
||||
}
|
||||
|
||||
impl SidebarTab {
|
||||
pub const ALL: [SidebarTab; 3] = [Self::Recents, Self::Chats, Self::Communities];
|
||||
pub const ALL: [SidebarTab; 2] = [Self::Inbox, Self::Communities];
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Recents => "Recents",
|
||||
Self::Chats => "Chats",
|
||||
Self::Inbox => "Inbox",
|
||||
Self::Communities => "Communities",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(self) -> IconName {
|
||||
match self {
|
||||
Self::Recents => IconName::History,
|
||||
Self::Chats => IconName::Message,
|
||||
Self::Communities => IconName::Group,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn list_id(self) -> &'static str {
|
||||
match self {
|
||||
Self::Recents => "sidebar-recents",
|
||||
Self::Chats => "sidebar-chats",
|
||||
Self::Inbox => "sidebar-inbox",
|
||||
Self::Communities => "sidebar-communities",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn index(self) -> usize {
|
||||
match self {
|
||||
Self::Recents => 0,
|
||||
Self::Chats => 1,
|
||||
Self::Communities => 2,
|
||||
Self::Inbox => 0,
|
||||
Self::Communities => 1,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn chat(self) -> bool {
|
||||
matches!(self, Self::Chats)
|
||||
}
|
||||
|
||||
pub fn community(self) -> bool {
|
||||
matches!(self, Self::Communities)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct TabBar {
|
||||
active: SidebarTab,
|
||||
on_select: Option<Rc<dyn Fn(SidebarTab, &mut Window, &mut App)>>,
|
||||
}
|
||||
|
||||
impl TabBar {
|
||||
pub fn new(active: SidebarTab) -> Self {
|
||||
Self {
|
||||
active,
|
||||
on_select: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_select(
|
||||
mut self,
|
||||
handler: impl Fn(SidebarTab, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_select = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for TabBar {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let Self { active, on_select } = self;
|
||||
|
||||
div()
|
||||
.id("sidebar-tabs")
|
||||
.absolute()
|
||||
.bottom_3()
|
||||
.left_0()
|
||||
.w_full()
|
||||
.px_4()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.p_1()
|
||||
.gap_1()
|
||||
.rounded_full()
|
||||
.bg(cx.theme().background)
|
||||
.when(cx.theme().shadow, |this| this.shadow_md())
|
||||
.children(SidebarTab::ALL.into_iter().map(|tab| {
|
||||
let on_select = on_select.clone();
|
||||
|
||||
Button::new(format!("tab-{}", tab.list_id()))
|
||||
.icon(tab.icon())
|
||||
.ghost()
|
||||
.flex_1()
|
||||
.rounded()
|
||||
.selected(tab == active)
|
||||
.tooltip(tab.label())
|
||||
.on_click(move |_event, window, cx| {
|
||||
if let Some(on_select) = on_select.as_ref() {
|
||||
on_select(tab, window, cx);
|
||||
}
|
||||
})
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,45 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use chat::Room;
|
||||
use community::Community;
|
||||
use community::{ChannelId, Community};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
App, ClickEvent, ElementId, Entity, ImageSource, InteractiveElement, IntoElement,
|
||||
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window, div, px,
|
||||
};
|
||||
use nostr_sdk::prelude::PublicKey;
|
||||
use settings::AppSettings;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::{Avatar, PixelAvatar};
|
||||
use ui::{Icon, IconName, Selectable, Sizable, StyledExt, h_flex};
|
||||
|
||||
use super::tab::SidebarTab;
|
||||
|
||||
pub enum SidebarRow {
|
||||
Section {
|
||||
label: SharedString,
|
||||
count: usize,
|
||||
Section { label: SharedString },
|
||||
Room { room: Entity<Room> },
|
||||
Community { community: Entity<Community> },
|
||||
Hint { text: SharedString },
|
||||
}
|
||||
|
||||
/// A collapsible group of rows in the sidebar's community view.
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CommunitySection {
|
||||
Channels,
|
||||
Admins,
|
||||
Members,
|
||||
}
|
||||
|
||||
/// A row in the sidebar's community view.
|
||||
pub enum CommunityRow {
|
||||
Section(CommunitySection),
|
||||
Channel {
|
||||
id: ChannelId,
|
||||
name: SharedString,
|
||||
private: bool,
|
||||
selected: bool,
|
||||
},
|
||||
Room {
|
||||
room: Entity<Room>,
|
||||
},
|
||||
Community {
|
||||
community: Entity<Community>,
|
||||
},
|
||||
Action {
|
||||
label: SharedString,
|
||||
tab: SidebarTab,
|
||||
},
|
||||
Hint {
|
||||
text: SharedString,
|
||||
Member {
|
||||
prefix: &'static str,
|
||||
public_key: PublicKey,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -39,7 +48,6 @@ pub enum TreeRowKind {
|
||||
Section,
|
||||
Room,
|
||||
Community,
|
||||
Action,
|
||||
Hint,
|
||||
}
|
||||
|
||||
@@ -51,7 +59,6 @@ pub struct TreeRow {
|
||||
avatar: Option<SharedString>,
|
||||
picture: Option<ImageSource>,
|
||||
icon: Option<IconName>,
|
||||
count: Option<usize>,
|
||||
created_at: Option<SharedString>,
|
||||
selected: bool,
|
||||
#[allow(clippy::type_complexity)]
|
||||
@@ -71,7 +78,6 @@ impl TreeRow {
|
||||
avatar: None,
|
||||
picture: None,
|
||||
icon: None,
|
||||
count: None,
|
||||
created_at: None,
|
||||
selected: false,
|
||||
on_click: None,
|
||||
@@ -96,11 +102,6 @@ impl TreeRow {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn count(mut self, count: usize) -> Self {
|
||||
self.count = Some(count);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn created_at(mut self, created_at: impl Into<SharedString>) -> Self {
|
||||
self.created_at = Some(created_at.into());
|
||||
self
|
||||
@@ -133,7 +134,6 @@ impl RenderOnce for TreeRow {
|
||||
let is_section = self.kind == TreeRowKind::Section;
|
||||
let is_room = self.kind == TreeRowKind::Room;
|
||||
let is_community = self.kind == TreeRowKind::Community;
|
||||
let is_action = self.kind == TreeRowKind::Action;
|
||||
let is_hint = self.kind == TreeRowKind::Hint;
|
||||
let is_selected = self.selected;
|
||||
|
||||
@@ -172,7 +172,6 @@ impl RenderOnce for TreeRow {
|
||||
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.h_8()
|
||||
.w_full()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
@@ -182,10 +181,8 @@ impl RenderOnce for TreeRow {
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.font_semibold()
|
||||
})
|
||||
.when(is_room || is_community, |this| this.text_sm())
|
||||
.when(is_action, |this| {
|
||||
this.text_sm().text_color(cx.theme().text_muted)
|
||||
})
|
||||
.when(is_room || is_community, |this| this.text_sm().h_10())
|
||||
.h_8()
|
||||
.when(is_hint, |this| {
|
||||
this.text_xs()
|
||||
.font_normal()
|
||||
@@ -203,6 +200,7 @@ impl RenderOnce for TreeRow {
|
||||
.when(is_room, |this| this.font_medium())
|
||||
.child(self.label),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.when(is_selected, |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::CheckCircle)
|
||||
@@ -211,11 +209,8 @@ impl RenderOnce for TreeRow {
|
||||
.text_color(cx.theme().icon_accent),
|
||||
)
|
||||
})
|
||||
.when_some(self.count, |this, count| {
|
||||
this.child(div().flex_shrink_0().font_normal().child(count.to_string()))
|
||||
})
|
||||
.when_some(self.created_at, |this, created_at| {
|
||||
this.child(div().flex_1()).child(
|
||||
this.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
|
||||
Reference in New Issue
Block a user