432 lines
20 KiB
Rust
432 lines
20 KiB
Rust
use std::ops::Range;
|
|
|
|
use chat::{Room, RoomKind};
|
|
use common::TimestampExt;
|
|
use gpui::prelude::FluentBuilder;
|
|
use gpui::{
|
|
AnyElement, App, Context, Entity, InteractiveElement as _, IntoElement, ParentElement,
|
|
SharedString, Styled, WeakEntity, Window, div, px,
|
|
};
|
|
use gpui_kit::assets::IconName;
|
|
use gpui_kit::component::avatar::Avatar;
|
|
use gpui_kit::component::dialog::DialogButtonProps;
|
|
use gpui_kit::component::dock::{ClosePanel, DockArea, DockPlacement, PanelHandle};
|
|
use gpui_kit::component::menu::{ContextMenuExt as _, PopupMenuItem};
|
|
use gpui_kit::component::{ActiveTheme, Icon, Selectable, Sizable, WindowExt, h_flex, v_flex};
|
|
use nip29::Group;
|
|
use nostr_sdk::prelude::*;
|
|
use settings::AppSettings;
|
|
use ui::{TextStyle, Typeset as _, button_radius};
|
|
|
|
use super::Sidebar;
|
|
use super::list::ListSection;
|
|
use super::search::MessageHit;
|
|
use crate::dialogs::{profile, screening};
|
|
use crate::nav::Nav;
|
|
|
|
/// Open a room in the dock, screening new requests when enabled.
|
|
pub(crate) fn open_room(
|
|
room: &Entity<Room>,
|
|
dock: &WeakEntity<DockArea>,
|
|
window: &mut Window,
|
|
cx: &mut App,
|
|
) {
|
|
crate::dock::add_panel_to(
|
|
dock,
|
|
PanelHandle::new(chat_ui::init(room.downgrade(), window, cx)),
|
|
DockPlacement::Center,
|
|
window,
|
|
cx,
|
|
);
|
|
|
|
if room.read(cx).kind != RoomKind::Ongoing && AppSettings::get_screening(cx) {
|
|
let peer = room.read(cx).display_member(cx).public_key();
|
|
let view = screening::init(peer, window, cx);
|
|
|
|
window.open_alert_dialog(cx, move |alert, _window, _cx| {
|
|
alert
|
|
.confirm()
|
|
.child(view.clone())
|
|
.button_props(
|
|
DialogButtonProps::default()
|
|
.cancel_text("Ignore")
|
|
.ok_text("Response"),
|
|
)
|
|
.on_cancel(move |_event, window, cx| {
|
|
window.dispatch_action(Box::new(ClosePanel), cx);
|
|
true
|
|
})
|
|
});
|
|
}
|
|
}
|
|
|
|
impl Sidebar {
|
|
pub(super) fn render_rows(
|
|
&self,
|
|
range: Range<usize>,
|
|
rows: &[SidebarRow],
|
|
cx: &Context<Sidebar>,
|
|
) -> Vec<AnyElement> {
|
|
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
|
|
|
rows.get(range.clone())
|
|
.into_iter()
|
|
.flatten()
|
|
.enumerate()
|
|
.map(|(offset, row)| {
|
|
let index = range.start + offset;
|
|
|
|
match row {
|
|
SidebarRow::Section(section) => {
|
|
let section = *section;
|
|
let label = section.label();
|
|
|
|
let open = match section {
|
|
ListSection::Pins => self.pins_open,
|
|
ListSection::Groups => self.is_groups_open(cx),
|
|
ListSection::Chats => self.is_chats_open(cx),
|
|
};
|
|
|
|
let icon = if open {
|
|
IconName::ChevronDown
|
|
} else {
|
|
IconName::ChevronRight
|
|
};
|
|
|
|
Nav::new(label)
|
|
.label(label)
|
|
.suffix(
|
|
Icon::new(icon)
|
|
.small()
|
|
.text_color(cx.theme().muted_foreground),
|
|
)
|
|
.text_style(TextStyle::Caption)
|
|
.font_weight(gpui::FontWeight::SEMIBOLD)
|
|
.text_color(cx.theme().muted_foreground)
|
|
.on_click(cx.listener(move |this, _event, _window, cx| {
|
|
match section {
|
|
ListSection::Pins => this.pins_open = !this.pins_open,
|
|
ListSection::Groups => {
|
|
this.groups_open = Some(!this.is_groups_open(cx))
|
|
}
|
|
ListSection::Chats => {
|
|
this.chats_open = Some(!this.is_chats_open(cx))
|
|
}
|
|
}
|
|
cx.notify();
|
|
}))
|
|
.into_any_element()
|
|
}
|
|
SidebarRow::Heading(label) => {
|
|
let label = *label;
|
|
|
|
Nav::new(label)
|
|
.label(label)
|
|
.text_style(TextStyle::Caption)
|
|
.font_weight(gpui::FontWeight::SEMIBOLD)
|
|
.text_color(cx.theme().muted_foreground)
|
|
.into_any_element()
|
|
}
|
|
SidebarRow::Empty(text) => v_flex()
|
|
.h_10()
|
|
.w_full()
|
|
.px_1p5()
|
|
.items_center()
|
|
.child(
|
|
h_flex()
|
|
.h_9()
|
|
.w_full()
|
|
.justify_center()
|
|
.border_1()
|
|
.border_dashed()
|
|
.border_color(cx.theme().border)
|
|
.rounded(px(button_radius()))
|
|
.text_style(TextStyle::Caption)
|
|
.text_color(cx.theme().muted_foreground)
|
|
.child(*text),
|
|
)
|
|
.into_any_element(),
|
|
SidebarRow::Room { room } => {
|
|
let id = room.read(cx).id;
|
|
let name = room.read(cx).display_name(cx);
|
|
let picture = room.read(cx).display_image(cx);
|
|
#[allow(unused_variables)]
|
|
let seed = room.read(cx).display_image_seed(cx);
|
|
let created_at = room.read(cx).created_at.to_ago();
|
|
let member = room.read(cx).members().first().copied();
|
|
let spam =
|
|
room.read(cx).is_spam(cx) && room.read(cx).kind == RoomKind::Request;
|
|
|
|
let pinned = AppSettings::global(cx).read(cx).pinned_room(id, cx);
|
|
let dock = self.dock.clone();
|
|
let room = room.clone();
|
|
|
|
let nav = Nav::new(index)
|
|
.label(name)
|
|
.text_style(TextStyle::Callout)
|
|
.when(!hide_avatar, |this| {
|
|
this.prefix(match picture {
|
|
Some(picture) => {
|
|
Avatar::new().src(picture).small().into_any_element()
|
|
}
|
|
None => {
|
|
Avatar::new().small().flex_shrink_0().into_any_element()
|
|
}
|
|
})
|
|
})
|
|
.suffix(
|
|
h_flex()
|
|
.gap_1()
|
|
.when(spam, |this| {
|
|
this.child(
|
|
div()
|
|
.px_1()
|
|
.rounded(px(button_radius()))
|
|
.bg(cx.theme().danger.opacity(0.15))
|
|
.text_size(px(8.0))
|
|
.text_color(cx.theme().danger)
|
|
.child("SPAM"),
|
|
)
|
|
})
|
|
.when(!spam, |this| {
|
|
this.child(
|
|
div()
|
|
.font_weight(gpui::FontWeight::NORMAL)
|
|
.text_style(TextStyle::Caption)
|
|
.text_color(cx.theme().muted_foreground)
|
|
.child(created_at),
|
|
)
|
|
}),
|
|
)
|
|
.on_click(move |_event, window, cx| {
|
|
open_room(&room, &dock, window, cx);
|
|
});
|
|
|
|
div()
|
|
.id(index)
|
|
.w_full()
|
|
.child(nav)
|
|
.context_menu(move |menu, _window, _cx| {
|
|
let menu = menu.item(
|
|
PopupMenuItem::new(if pinned { "Unpin" } else { "Pin" })
|
|
.on_click(move |_, _window, cx| {
|
|
AppSettings::global(cx).update(cx, |settings, cx| {
|
|
settings.toggle_pinned_room(id, cx);
|
|
});
|
|
}),
|
|
);
|
|
|
|
if let Some(member) = member {
|
|
menu.item(PopupMenuItem::new("View profile").on_click(
|
|
move |_, window, cx| {
|
|
profile::open(member, window, cx);
|
|
},
|
|
))
|
|
} else {
|
|
menu
|
|
}
|
|
})
|
|
.into_any_element()
|
|
}
|
|
SidebarRow::Contact { public_key } => {
|
|
let person = self.directory.read(cx).person(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(index)
|
|
.label(person.name())
|
|
.text_style(TextStyle::Callout)
|
|
.when(!hide_avatar, |this| {
|
|
this.prefix(match person.avatar() {
|
|
Some(picture) => {
|
|
Avatar::new().src(picture).small().into_any_element()
|
|
}
|
|
None => {
|
|
Avatar::new().small().flex_shrink_0().into_any_element()
|
|
}
|
|
})
|
|
})
|
|
.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::Profile { public_key } => {
|
|
let person = self.directory.read(cx).person(public_key, cx);
|
|
let public_key = public_key.to_owned();
|
|
|
|
Nav::new(index)
|
|
.label(person.name())
|
|
.text_style(TextStyle::Callout)
|
|
.when(!hide_avatar, |this| {
|
|
this.prefix(match person.avatar() {
|
|
Some(picture) => {
|
|
Avatar::new().src(picture).small().into_any_element()
|
|
}
|
|
None => {
|
|
Avatar::new().small().flex_shrink_0().into_any_element()
|
|
}
|
|
})
|
|
})
|
|
.on_click(move |_event, window, cx| {
|
|
profile::open(public_key, window, cx);
|
|
})
|
|
.into_any_element()
|
|
}
|
|
SidebarRow::Group { group } => {
|
|
let dock = self.dock.clone();
|
|
let sidebar = cx.entity().downgrade();
|
|
let group = group.clone();
|
|
|
|
let name = group.read(cx).display_name();
|
|
let picture = group.read(cx).display_image();
|
|
let seed = group.read(cx).key().cache_tag();
|
|
let tag = seed.clone();
|
|
let pinned = AppSettings::global(cx).read(cx).pinned_group(&tag, cx);
|
|
let weak_group = group.downgrade();
|
|
|
|
let suffix = group
|
|
.read(cx)
|
|
.membership()
|
|
.status()
|
|
.or_else(|| group.read(cx).elsewhere().map(|_| "Moved"))
|
|
.map(SharedString::from);
|
|
|
|
let nav = Nav::new(index)
|
|
.label(name)
|
|
.text_style(TextStyle::Callout)
|
|
.when(!hide_avatar, |this| {
|
|
this.prefix(match picture {
|
|
Some(picture) => {
|
|
Avatar::new().src(picture).small().into_any_element()
|
|
}
|
|
None => {
|
|
Avatar::new().small().flex_shrink_0().into_any_element()
|
|
}
|
|
})
|
|
})
|
|
.when_some(suffix, |this, suffix| {
|
|
this.suffix(
|
|
div()
|
|
.font_weight(gpui::FontWeight::NORMAL)
|
|
.text_style(TextStyle::Caption)
|
|
.text_color(cx.theme().muted_foreground)
|
|
.child(suffix),
|
|
)
|
|
})
|
|
.on_click(move |_event, window, cx| {
|
|
crate::dock::add_panel_to(
|
|
&dock,
|
|
PanelHandle::new(nip29_ui::init(group.clone(), window, cx)),
|
|
DockPlacement::Center,
|
|
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}");
|
|
}
|
|
});
|
|
|
|
div()
|
|
.id(index)
|
|
.w_full()
|
|
.child(nav)
|
|
.context_menu(move |menu, _window, _cx| {
|
|
menu.item(
|
|
PopupMenuItem::new(if pinned { "Unpin" } else { "Pin" })
|
|
.on_click({
|
|
let tag = tag.clone();
|
|
move |_, _window, cx| {
|
|
AppSettings::global(cx).update(
|
|
cx,
|
|
|settings, cx| {
|
|
settings.toggle_pinned_group(&tag, cx);
|
|
},
|
|
);
|
|
}
|
|
}),
|
|
)
|
|
.item(
|
|
PopupMenuItem::new("Leave").on_click({
|
|
let group = weak_group.clone();
|
|
move |_, window, cx| {
|
|
nip29_ui::confirm_leave(group.clone(), window, cx);
|
|
}
|
|
}),
|
|
)
|
|
})
|
|
.into_any_element()
|
|
}
|
|
SidebarRow::Message { hit } => {
|
|
let person = self.directory.read(cx).person(&hit.author, cx);
|
|
let name = hit.room.read(cx).display_name(cx);
|
|
let detail = format!("{} · {}", name, hit.created_at.to_ago());
|
|
let dock = self.dock.clone();
|
|
let room = hit.room.clone();
|
|
let content = hit.content.clone();
|
|
|
|
Nav::new(index)
|
|
.label(content)
|
|
.text_style(TextStyle::Callout)
|
|
.font_weight(gpui::FontWeight::MEDIUM)
|
|
.when(!hide_avatar, |this| {
|
|
this.prefix(match person.avatar() {
|
|
Some(picture) => {
|
|
Avatar::new().src(picture).small().into_any_element()
|
|
}
|
|
None => {
|
|
Avatar::new().small().flex_shrink_0().into_any_element()
|
|
}
|
|
})
|
|
})
|
|
.suffix(
|
|
div()
|
|
.font_weight(gpui::FontWeight::NORMAL)
|
|
.text_style(TextStyle::Caption)
|
|
.text_color(cx.theme().muted_foreground)
|
|
.child(detail),
|
|
)
|
|
.on_click(move |_event, window, cx| {
|
|
open_room(&room, &dock, window, cx);
|
|
})
|
|
.into_any_element()
|
|
}
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
/// A row shown in the sidebar's lists.
|
|
pub(super) enum SidebarRow {
|
|
Section(ListSection),
|
|
Room {
|
|
room: Entity<Room>,
|
|
},
|
|
Group {
|
|
group: Entity<Group>,
|
|
},
|
|
Contact {
|
|
public_key: PublicKey,
|
|
},
|
|
/// A profile found by global search.
|
|
Profile {
|
|
public_key: PublicKey,
|
|
},
|
|
Message {
|
|
hit: MessageHit,
|
|
},
|
|
/// A non-interactive label, such as a search results section.
|
|
Heading(&'static str),
|
|
Empty(&'static str),
|
|
}
|