add contact panel
This commit is contained in:
@@ -101,8 +101,8 @@ impl RenderOnce for Nav {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.flex_1()
|
||||
.child(div().truncate().min_w_0().child(self.label))
|
||||
.child(div().flex_1())
|
||||
.min_w_0()
|
||||
.child(div().flex_1().min_w_0().truncate().child(self.label))
|
||||
.when_some(self.suffix, |this, suffix| {
|
||||
this.child(div().flex_shrink_0().child(suffix))
|
||||
}),
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
use std::collections::HashSet;
|
||||
use std::ops::Range;
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::Error;
|
||||
use auto_update::AutoUpdater;
|
||||
use chat::{ChatEvent, ChatRegistry, Room, RoomKind};
|
||||
use common::TimestampExt;
|
||||
use community::{ChannelId, Community, CommunityEvent, CommunityRegistry};
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, Div, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
AnyElement, App, AppContext, Context, Div, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ObjectFit, ParentElement, Render, SharedString, Stateful,
|
||||
Styled, StyledImage, Subscription, UniformListScrollHandle, WeakEntity, Window, div, img, px,
|
||||
retain_all, uniform_list,
|
||||
Styled, StyledImage, Subscription, Task, UniformListScrollHandle, WeakEntity, Window, div, img,
|
||||
px, retain_all, uniform_list,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::PersonRegistry;
|
||||
@@ -45,6 +47,7 @@ pub(crate) use utils::{nav_avatar, nav_icon, pick_banner};
|
||||
pub enum SidebarRow {
|
||||
Room { room: Entity<Room> },
|
||||
Community { community: Entity<Community> },
|
||||
Contact { public_key: PublicKey },
|
||||
}
|
||||
|
||||
/// A collapsible group of rows in the sidebar's community view.
|
||||
@@ -81,10 +84,19 @@ pub struct Sidebar {
|
||||
active_tab: SidebarTab,
|
||||
/// The community the sidebar is browsing, if any
|
||||
community: Option<WeakEntity<Community>>,
|
||||
/// 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
|
||||
contacts: Option<Vec<PublicKey>>,
|
||||
/// Contacts selected in the contact picker
|
||||
selected_contacts: HashSet<PublicKey>,
|
||||
/// Scroll state of the contact picker's list
|
||||
contacts_scroll: UniformListScrollHandle,
|
||||
channels_open: bool,
|
||||
admins_open: bool,
|
||||
members_open: bool,
|
||||
new_requests: bool,
|
||||
tasks: SmallVec<[Task<Result<(), Error>>; 1]>,
|
||||
_subscriptions: SmallVec<[Subscription; 4]>,
|
||||
}
|
||||
|
||||
@@ -136,10 +148,15 @@ impl Sidebar {
|
||||
banner: pick_banner(),
|
||||
active_tab: SidebarTab::Inbox,
|
||||
community: None,
|
||||
contacts_open: false,
|
||||
contacts: None,
|
||||
selected_contacts: HashSet::new(),
|
||||
contacts_scroll: UniformListScrollHandle::new(),
|
||||
channels_open: true,
|
||||
admins_open: true,
|
||||
members_open: true,
|
||||
new_requests: false,
|
||||
tasks: smallvec![],
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
@@ -149,9 +166,134 @@ impl Sidebar {
|
||||
return;
|
||||
}
|
||||
self.active_tab = tab;
|
||||
self.close_contacts();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Leave the contact picker and clear its selection.
|
||||
fn close_contacts(&mut self) {
|
||||
self.contacts_open = false;
|
||||
self.selected_contacts.clear();
|
||||
}
|
||||
|
||||
/// Show or hide the contact picker in the inbox list.
|
||||
fn toggle_contacts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.contacts_open = !self.contacts_open;
|
||||
|
||||
if !self.contacts_open {
|
||||
self.selected_contacts.clear();
|
||||
} else if self.contacts.is_none() {
|
||||
self.load_contacts(window, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Load the signed-in user's contact list into the contact picker.
|
||||
fn load_contacts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
|
||||
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let task: Task<Result<Vec<PublicKey>, Error>> = cx.background_spawn(async move {
|
||||
let filter = Filter::new()
|
||||
.author(public_key)
|
||||
.kind(Kind::ContactList)
|
||||
.limit(1);
|
||||
|
||||
let contacts: Vec<PublicKey> = client
|
||||
.database()
|
||||
.query(filter)
|
||||
.await?
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|event| event.tags.public_keys().collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(contacts)
|
||||
});
|
||||
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
match task.await {
|
||||
Ok(contacts) => {
|
||||
this.update(cx, |this, cx| {
|
||||
this.contacts = Some(contacts);
|
||||
cx.notify();
|
||||
})?;
|
||||
}
|
||||
Err(error) => {
|
||||
cx.update(|window, cx| {
|
||||
window.push_notification(
|
||||
Notification::error(error.to_string()).autohide(false),
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Toggle a contact's selection in the contact picker.
|
||||
fn toggle_contact(&mut self, public_key: &PublicKey, cx: &mut Context<Self>) {
|
||||
if !self.selected_contacts.remove(public_key) {
|
||||
self.selected_contacts.insert(public_key.to_owned());
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Start a chat with the contacts selected in the picker.
|
||||
fn create_chat(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.selected_contacts.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let Some(public_key) = nostr.read(cx).current_user() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let chat = ChatRegistry::global(cx);
|
||||
let async_chat = chat.downgrade();
|
||||
let dock = self.dock.clone();
|
||||
|
||||
let receivers: Vec<PublicKey> = self.selected_contacts.iter().copied().collect();
|
||||
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
let room = async_chat.update_in(cx, |chat, _window, cx| {
|
||||
let room = cx.new(|_| {
|
||||
Room::new(public_key, receivers)
|
||||
.organize(&public_key)
|
||||
.kind(RoomKind::Ongoing)
|
||||
});
|
||||
chat.track_room(&room, cx);
|
||||
room
|
||||
})?;
|
||||
|
||||
cx.update(|window, cx| {
|
||||
ui::dock::add_panel_to(
|
||||
&dock,
|
||||
PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)),
|
||||
DockPlacement::Center,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})?;
|
||||
|
||||
// Leave the picker now that the chat is open
|
||||
this.update(cx, |this, cx| {
|
||||
this.close_contacts();
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
@@ -293,13 +435,30 @@ impl Sidebar {
|
||||
fn render_tabs(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
||||
let sidebar = cx.entity().downgrade();
|
||||
let active_tab = self.active_tab;
|
||||
let contacts_open = self.contacts_open && active_tab == SidebarTab::Inbox;
|
||||
let rows = Rc::new(self.rows_for(active_tab, cx));
|
||||
let scroll_handle = &self.scroll_handles[active_tab.index()];
|
||||
let create_button = contacts_open && !self.selected_contacts.is_empty();
|
||||
|
||||
let (list_id, scroll_handle) = if contacts_open {
|
||||
("sidebar-contacts", &self.contacts_scroll)
|
||||
} else {
|
||||
(
|
||||
active_tab.list_id(),
|
||||
&self.scroll_handles[active_tab.index()],
|
||||
)
|
||||
};
|
||||
|
||||
let list_title = if contacts_open {
|
||||
"Contacts"
|
||||
} else {
|
||||
active_tab.list_title()
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
.relative()
|
||||
.gap_2()
|
||||
.child(
|
||||
div().px_2().child(
|
||||
@@ -358,9 +517,12 @@ impl Sidebar {
|
||||
)
|
||||
.child(
|
||||
NavItem::new("contacts", "Contacts", Icon::new(IconName::Book).small())
|
||||
.on_click(|_event, window, cx| {
|
||||
window.dispatch_action(Box::new(Command::ShowContactList), cx)
|
||||
}),
|
||||
.when(contacts_open, |this| {
|
||||
this.bg(cx.theme().ghost_element_active)
|
||||
})
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.toggle_contacts(window, cx);
|
||||
})),
|
||||
),
|
||||
),
|
||||
SidebarTab::Communities => this.child(
|
||||
@@ -391,7 +553,7 @@ impl Sidebar {
|
||||
.text_xs()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.child(active_tab.list_title()),
|
||||
.child(list_title),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -399,7 +561,7 @@ impl Sidebar {
|
||||
.flex_1()
|
||||
.child(
|
||||
uniform_list(
|
||||
active_tab.list_id(),
|
||||
list_id,
|
||||
rows.len(),
|
||||
cx.processor(move |this, range, _window, cx| {
|
||||
this.render_rows(range, rows.as_slice(), cx)
|
||||
@@ -411,6 +573,43 @@ impl Sidebar {
|
||||
)
|
||||
.child(Scrollbar::vertical(scroll_handle)),
|
||||
)
|
||||
.when(create_button, |this| {
|
||||
this.child(
|
||||
v_flex()
|
||||
.absolute()
|
||||
.bottom_0()
|
||||
.left_0()
|
||||
.justify_center()
|
||||
.h_20()
|
||||
.w_full()
|
||||
.px_4()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().border)
|
||||
.bg(cx.theme().background)
|
||||
.child(
|
||||
Button::new("create-chat")
|
||||
.label(if self.selected_contacts.len() > 1 {
|
||||
"Create Group Chat"
|
||||
} else {
|
||||
"Create Chat"
|
||||
})
|
||||
.primary()
|
||||
.on_click(cx.listener(|this, _ev, window, cx| {
|
||||
this.create_chat(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().text_muted)
|
||||
.child("Selected")
|
||||
.child(div().font_semibold().child(SharedString::from(
|
||||
self.selected_contacts.len().to_string(),
|
||||
))),
|
||||
),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -537,6 +736,13 @@ impl Sidebar {
|
||||
|
||||
fn rows_for(&self, tab: SidebarTab, cx: &App) -> Vec<SidebarRow> {
|
||||
match tab {
|
||||
SidebarTab::Inbox if self.contacts_open => self
|
||||
.contacts
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|public_key| SidebarRow::Contact { public_key })
|
||||
.collect(),
|
||||
SidebarTab::Inbox => {
|
||||
let chat = ChatRegistry::global(cx);
|
||||
chat.read(cx)
|
||||
@@ -605,6 +811,31 @@ impl Sidebar {
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarRow::Contact { public_key } => {
|
||||
let persons = PersonRegistry::global(cx);
|
||||
let person = persons.read(cx).get(public_key, cx);
|
||||
let selected = self.selected_contacts.contains(public_key);
|
||||
let sidebar = cx.entity().downgrade();
|
||||
let public_key = public_key.to_owned();
|
||||
|
||||
Nav::new(SharedString::from(format!("contact-{index}")))
|
||||
.label(person.name())
|
||||
.text_sm()
|
||||
.font_medium()
|
||||
.when_some(
|
||||
nav_avatar(Some(person.avatar_seed()), person.avatar(), cx),
|
||||
|this, avatar| this.prefix(avatar),
|
||||
)
|
||||
.selected(selected)
|
||||
.on_click(move |_event, _window, cx| {
|
||||
if let Err(error) = sidebar.update(cx, |this, cx| {
|
||||
this.toggle_contact(&public_key, cx);
|
||||
}) {
|
||||
log::error!("Failed to select contact: {error}");
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
SidebarRow::Community { community } => {
|
||||
let name = community.read(cx).name();
|
||||
let seed = community.read(cx).id().to_hex();
|
||||
|
||||
Reference in New Issue
Block a user