feat: Redesign New Chat (#31)
* make subject is optional * redesign * search * fix * adjust
This commit is contained in:
@@ -32,9 +32,9 @@ rust-embed.workspace = true
|
||||
log.workspace = true
|
||||
smallvec.workspace = true
|
||||
smol.workspace = true
|
||||
futures.workspace = true
|
||||
oneshot.workspace = true
|
||||
|
||||
webbrowser = "1.0.4"
|
||||
rustls = "0.23.23"
|
||||
futures = "0.3"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["fmt"] }
|
||||
|
||||
@@ -20,12 +20,10 @@ use ui::{
|
||||
|
||||
use crate::{
|
||||
lru_cache::cache_provider,
|
||||
views::{
|
||||
chat, compose, login, new_account, onboarding, profile, relays, search, sidebar, welcome,
|
||||
},
|
||||
views::{chat, compose, login, new_account, onboarding, profile, relays, sidebar, welcome},
|
||||
};
|
||||
|
||||
const CACHE_SIZE: usize = 200;
|
||||
const IMAGE_CACHE_SIZE: usize = 200;
|
||||
const MODAL_WIDTH: f32 = 420.;
|
||||
const SIDEBAR_WIDTH: f32 = 280.;
|
||||
|
||||
@@ -53,7 +51,6 @@ pub enum PanelKind {
|
||||
pub enum ModalKind {
|
||||
Profile,
|
||||
Compose,
|
||||
Search,
|
||||
Relay,
|
||||
Onboarding,
|
||||
SetupRelay,
|
||||
@@ -242,16 +239,6 @@ impl ChatSpace {
|
||||
.child(compose.clone())
|
||||
})
|
||||
}
|
||||
ModalKind::Search => {
|
||||
let search = search::init(window, cx);
|
||||
|
||||
window.open_modal(cx, move |modal, _, _| {
|
||||
modal
|
||||
.closable(false)
|
||||
.width(px(MODAL_WIDTH))
|
||||
.child(search.clone())
|
||||
})
|
||||
}
|
||||
ModalKind::Relay => {
|
||||
let relays = relays::init(window, cx);
|
||||
|
||||
@@ -299,7 +286,7 @@ impl Render for ChatSpace {
|
||||
.relative()
|
||||
.size_full()
|
||||
.child(
|
||||
image_cache(cache_provider("image-cache", CACHE_SIZE))
|
||||
image_cache(cache_provider("image-cache", IMAGE_CACHE_SIZE))
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
|
||||
@@ -148,22 +148,19 @@ impl Chat {
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
if let Ok(result) = task.await {
|
||||
cx.update(|_, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
result.into_iter().for_each(|item| {
|
||||
if !item.1 {
|
||||
let profile = this
|
||||
.room
|
||||
.read_with(cx, |this, _| this.profile_by_pubkey(&item.0, cx));
|
||||
this.update(cx, |this, cx| {
|
||||
result.into_iter().for_each(|item| {
|
||||
if !item.1 {
|
||||
let profile = this
|
||||
.room
|
||||
.read_with(cx, |this, _| this.profile_by_pubkey(&item.0, cx));
|
||||
|
||||
this.push_system_message(
|
||||
format!("{} {}", profile.shared_name(), ALERT),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
this.push_system_message(
|
||||
format!("{} {}", profile.shared_name(), ALERT),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
@@ -235,8 +232,8 @@ impl Chat {
|
||||
|
||||
// Update input state
|
||||
self.input.update(cx, |this, cx| {
|
||||
this.set_loading(true, window, cx);
|
||||
this.set_disabled(true, window, cx);
|
||||
this.set_loading(true, cx);
|
||||
this.set_disabled(true, cx);
|
||||
});
|
||||
|
||||
let room = self.room.read(cx);
|
||||
@@ -261,8 +258,8 @@ impl Chat {
|
||||
cx.update(|window, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.input.update(cx, |this, cx| {
|
||||
this.set_loading(false, window, cx);
|
||||
this.set_disabled(false, window, cx);
|
||||
this.set_loading(false, cx);
|
||||
this.set_disabled(false, cx);
|
||||
this.set_text("", window, cx);
|
||||
});
|
||||
received = true;
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{
|
||||
|
||||
use anyhow::Error;
|
||||
use chats::ChatRegistry;
|
||||
use common::{profile::SharedProfile, random_name};
|
||||
use common::profile::SharedProfile;
|
||||
use global::get_client;
|
||||
use gpui::{
|
||||
div, img, impl_internal_actions, prelude::FluentBuilder, px, red, relative, uniform_list, App,
|
||||
@@ -56,14 +56,10 @@ impl Compose {
|
||||
let error_message = cx.new(|_| None);
|
||||
|
||||
let title_input = cx.new(|cx| {
|
||||
let name = random_name(2);
|
||||
let mut input = TextInput::new(window, cx)
|
||||
TextInput::new(window, cx)
|
||||
.appearance(false)
|
||||
.text_size(Size::Small);
|
||||
|
||||
input.set_placeholder("Family... . (Optional)");
|
||||
input.set_text(name, window, cx);
|
||||
input
|
||||
.placeholder("Family... . (Optional)")
|
||||
.text_size(Size::Small)
|
||||
});
|
||||
|
||||
let user_input = cx.new(|cx| {
|
||||
@@ -151,6 +147,7 @@ impl Compose {
|
||||
let event: Task<Result<Event, anyhow::Error>> = cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
let signer = client.signer().await?;
|
||||
|
||||
// [IMPORTANT]
|
||||
// Make sure this event is never send,
|
||||
// this event existed just use for convert to Coop's Room later.
|
||||
@@ -166,7 +163,7 @@ impl Compose {
|
||||
Ok(event) => {
|
||||
cx.update(|window, cx| {
|
||||
ChatRegistry::global(cx).update(cx, |chats, cx| {
|
||||
let id = chats.push(&event, window, cx);
|
||||
let id = chats.push_event(&event, window, cx);
|
||||
window.close_modal(cx);
|
||||
window.dispatch_action(
|
||||
Box::new(AddPanel::new(PanelKind::Room(id), DockPlacement::Center)),
|
||||
@@ -351,7 +348,7 @@ impl Render for Compose {
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.child(div().pb_0p5().text_sm().font_semibold().child("Title:"))
|
||||
.child(div().pb_0p5().text_sm().font_semibold().child("Subject:"))
|
||||
.child(self.title_input.clone()),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@ pub mod new_account;
|
||||
pub mod onboarding;
|
||||
pub mod profile;
|
||||
pub mod relays;
|
||||
pub mod search;
|
||||
pub mod sidebar;
|
||||
pub mod subject;
|
||||
pub mod welcome;
|
||||
|
||||
@@ -314,7 +314,7 @@ impl Render for Profile {
|
||||
.child(self.bio_input.clone()),
|
||||
)
|
||||
.child(
|
||||
div().p_3().child(
|
||||
div().py_3().child(
|
||||
Button::new("submit")
|
||||
.label("Update")
|
||||
.primary()
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder, App, ClickEvent, Div, InteractiveElement, IntoElement,
|
||||
ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::Icon;
|
||||
|
||||
type Handler = Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct SidebarButton {
|
||||
base: Div,
|
||||
label: SharedString,
|
||||
icon: Option<Icon>,
|
||||
handler: Handler,
|
||||
}
|
||||
|
||||
impl SidebarButton {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
base: div().flex().items_center().gap_3().px_3().h_8(),
|
||||
label: label.into(),
|
||||
icon: None,
|
||||
handler: Rc::new(|_, _, _| {}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
self.icon = Some(icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.handler = Rc::new(handler);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for SidebarButton {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let handler = self.handler.clone();
|
||||
|
||||
self.base
|
||||
.id(self.label.clone())
|
||||
.rounded(cx.theme().radius)
|
||||
.when_some(self.icon, |this, icon| {
|
||||
this.child(div().text_color(cx.theme().text_muted).child(icon))
|
||||
})
|
||||
.child(self.label.clone())
|
||||
.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
||||
.on_click(move |ev, window, cx| handler(ev, window, cx))
|
||||
}
|
||||
}
|
||||
@@ -299,7 +299,7 @@ impl RenderOnce for FolderItem {
|
||||
.font_medium()
|
||||
.map(|this| {
|
||||
if let Some(img) = self.img {
|
||||
this.child(img.size_5().flex_shrink_0())
|
||||
this.child(img.size_6().flex_shrink_0())
|
||||
} else {
|
||||
this.child(
|
||||
div()
|
||||
|
||||
@@ -1,27 +1,35 @@
|
||||
use std::{cmp::Reverse, collections::HashSet};
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
collections::{BTreeSet, HashSet},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use account::Account;
|
||||
use button::SidebarButton;
|
||||
use async_utility::task::spawn;
|
||||
use chats::{
|
||||
room::{Room, RoomKind},
|
||||
ChatRegistry,
|
||||
};
|
||||
use common::profile::SharedProfile;
|
||||
|
||||
use common::{debounced_delay::DebouncedDelay, profile::SharedProfile};
|
||||
use folder::{Folder, FolderItem, Parent};
|
||||
use global::get_client;
|
||||
use global::{constants::SEARCH_RELAYS, get_client};
|
||||
use gpui::{
|
||||
actions, div, img, prelude::FluentBuilder, AnyElement, App, AppContext, Context, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render,
|
||||
ScrollHandle, SharedString, StatefulInteractiveElement, Styled, Task, Window,
|
||||
div, img, prelude::FluentBuilder, AnyElement, App, AppContext, Context, Entity, EventEmitter,
|
||||
FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement, Render, ScrollHandle,
|
||||
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Window,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use nostr_sdk::prelude::*;
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{
|
||||
button::{Button, ButtonCustomVariant, ButtonVariants},
|
||||
button::{Button, ButtonCustomVariant, ButtonRounded, ButtonVariants},
|
||||
dock_area::{
|
||||
dock::DockPlacement,
|
||||
panel::{Panel, PanelEvent},
|
||||
},
|
||||
input::{InputEvent, TextInput},
|
||||
popup_menu::{PopupMenu, PopupMenuExt},
|
||||
skeleton::Skeleton,
|
||||
IconName, Sizable, StyledExt,
|
||||
@@ -29,10 +37,10 @@ use ui::{
|
||||
|
||||
use crate::chatspace::{AddPanel, ModalKind, PanelKind, ToggleModal};
|
||||
|
||||
mod button;
|
||||
mod folder;
|
||||
|
||||
actions!(profile, [Logout]);
|
||||
const FIND_DELAY: u64 = 600;
|
||||
const FIND_LIMIT: usize = 10;
|
||||
|
||||
pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
|
||||
Sidebar::new(window, cx)
|
||||
@@ -52,11 +60,21 @@ pub enum SubItem {
|
||||
|
||||
pub struct Sidebar {
|
||||
name: SharedString,
|
||||
// Search
|
||||
find_input: Entity<TextInput>,
|
||||
find_debouncer: DebouncedDelay<Self>,
|
||||
finding: bool,
|
||||
local_result: Entity<Option<Vec<Entity<Room>>>>,
|
||||
global_result: Entity<Option<Vec<Entity<Room>>>>,
|
||||
// Layout
|
||||
split_into_folders: bool,
|
||||
active_items: HashSet<Item>,
|
||||
active_subitems: HashSet<SubItem>,
|
||||
// GPUI
|
||||
focus_handle: FocusHandle,
|
||||
scroll_handle: ScrollHandle,
|
||||
#[allow(dead_code)]
|
||||
subscriptions: SmallVec<[Subscription; 1]>,
|
||||
}
|
||||
|
||||
impl Sidebar {
|
||||
@@ -64,7 +82,7 @@ impl Sidebar {
|
||||
cx.new(|cx| Self::view(window, cx))
|
||||
}
|
||||
|
||||
fn view(_window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
fn view(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let scroll_handle = ScrollHandle::default();
|
||||
|
||||
@@ -75,13 +93,65 @@ impl Sidebar {
|
||||
active_subitems.insert(SubItem::Trusted);
|
||||
active_subitems.insert(SubItem::Unknown);
|
||||
|
||||
let local_result = cx.new(|_| None);
|
||||
let global_result = cx.new(|_| None);
|
||||
let find_input = cx.new(|cx| {
|
||||
TextInput::new(window, cx)
|
||||
.small()
|
||||
.text_size(ui::Size::XSmall)
|
||||
.suffix(|window, cx| {
|
||||
Button::new("find")
|
||||
.icon(IconName::Search)
|
||||
.tooltip("Press Enter to search")
|
||||
.small()
|
||||
.custom(
|
||||
ButtonCustomVariant::new(window, cx)
|
||||
.active(gpui::transparent_black())
|
||||
.color(gpui::transparent_black())
|
||||
.hover(gpui::transparent_black())
|
||||
.foreground(cx.theme().text_placeholder),
|
||||
)
|
||||
})
|
||||
.placeholder("Find or start a conversation")
|
||||
});
|
||||
|
||||
let mut subscriptions = smallvec![];
|
||||
|
||||
subscriptions.push(
|
||||
cx.subscribe_in(&find_input, window, |this, _, event, _, cx| {
|
||||
match event {
|
||||
InputEvent::PressEnter => this.search(cx),
|
||||
InputEvent::Change(text) => {
|
||||
// Clear the result when input is empty
|
||||
if text.is_empty() {
|
||||
this.clear_search_results(cx);
|
||||
} else {
|
||||
// Run debounced search
|
||||
this.find_debouncer.fire_new(
|
||||
Duration::from_millis(FIND_DELAY),
|
||||
cx,
|
||||
|this, cx| this.debounced_search(cx),
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
Self {
|
||||
name: "Chat Sidebar".into(),
|
||||
split_into_folders: false,
|
||||
find_debouncer: DebouncedDelay::new(),
|
||||
finding: false,
|
||||
find_input,
|
||||
local_result,
|
||||
global_result,
|
||||
active_items,
|
||||
active_subitems,
|
||||
focus_handle,
|
||||
scroll_handle,
|
||||
subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,31 +169,166 @@ impl Sidebar {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn split_into_folders(&mut self, cx: &mut Context<Self>) {
|
||||
fn toggle_folder(&mut self, cx: &mut Context<Self>) {
|
||||
self.split_into_folders = !self.split_into_folders;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn on_logout(&mut self, _: &Logout, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let task: Task<Result<(), anyhow::Error>> = cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
_ = client.reset().await;
|
||||
fn debounced_search(&self, cx: &mut Context<Self>) -> Task<()> {
|
||||
cx.spawn(async move |this, cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
this.search(cx);
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
Ok(())
|
||||
fn nip50_search(&self, cx: &App) -> Task<Result<BTreeSet<Room>, Error>> {
|
||||
let query = self.find_input.read(cx).text();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let client = get_client();
|
||||
|
||||
let filter = Filter::new()
|
||||
.kind(Kind::Metadata)
|
||||
.search(query.to_lowercase())
|
||||
.limit(FIND_LIMIT);
|
||||
|
||||
let events = client
|
||||
.fetch_events_from(SEARCH_RELAYS, filter, Duration::from_secs(3))
|
||||
.await?
|
||||
.into_iter()
|
||||
.unique_by(|event| event.pubkey)
|
||||
.collect_vec();
|
||||
|
||||
let mut rooms = BTreeSet::new();
|
||||
let (tx, rx) = smol::channel::bounded::<Room>(10);
|
||||
|
||||
spawn(async move {
|
||||
let client = get_client();
|
||||
let signer = client.signer().await.unwrap();
|
||||
|
||||
for event in events.into_iter() {
|
||||
let metadata = Metadata::from_json(event.content).unwrap_or_default();
|
||||
|
||||
if let Some(target) = metadata.nip05.as_ref() {
|
||||
if let Ok(verify) = nip05::verify(&event.pubkey, target, None).await {
|
||||
if verify {
|
||||
if let Ok(event) = EventBuilder::private_msg_rumor(event.pubkey, "")
|
||||
.sign(&signer)
|
||||
.await
|
||||
{
|
||||
let room = Room::new(&event);
|
||||
_ = tx.send(room).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
while let Ok(room) = rx.recv().await {
|
||||
rooms.insert(room);
|
||||
}
|
||||
|
||||
Ok(rooms)
|
||||
})
|
||||
}
|
||||
|
||||
fn search(&mut self, cx: &mut Context<Self>) {
|
||||
let query = self.find_input.read(cx).text();
|
||||
let result = ChatRegistry::get_global(cx).search(query.as_ref(), cx);
|
||||
|
||||
// Return if query is empty
|
||||
if query.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Return if search is in progress
|
||||
if self.finding {
|
||||
return;
|
||||
}
|
||||
|
||||
// Block the UI until the search process completes
|
||||
self.set_finding(true, cx);
|
||||
|
||||
// Disable the search input to prevent duplicate requests
|
||||
self.find_input.update(cx, |this, cx| {
|
||||
this.set_disabled(true, cx);
|
||||
this.set_loading(true, cx);
|
||||
});
|
||||
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
if task.await.is_ok() {
|
||||
cx.update(|_, cx| {
|
||||
Account::global(cx).update(cx, |this, cx| {
|
||||
this.profile = None;
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
};
|
||||
})
|
||||
.detach();
|
||||
if !result.is_empty() {
|
||||
self.set_finding(false, cx);
|
||||
|
||||
self.find_input.update(cx, |this, cx| {
|
||||
this.set_disabled(false, cx);
|
||||
this.set_loading(false, cx);
|
||||
});
|
||||
|
||||
self.local_result.update(cx, |this, cx| {
|
||||
*this = Some(result);
|
||||
cx.notify();
|
||||
});
|
||||
} else {
|
||||
let task = self.nip50_search(cx);
|
||||
|
||||
cx.spawn(async move |this, cx| {
|
||||
if let Ok(result) = task.await {
|
||||
this.update(cx, |this, cx| {
|
||||
let result = result
|
||||
.into_iter()
|
||||
.map(|room| cx.new(|_| room))
|
||||
.collect_vec();
|
||||
|
||||
this.set_finding(false, cx);
|
||||
|
||||
this.find_input.update(cx, |this, cx| {
|
||||
this.set_disabled(false, cx);
|
||||
this.set_loading(false, cx);
|
||||
});
|
||||
|
||||
this.global_result.update(cx, |this, cx| {
|
||||
*this = Some(result);
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_finding(&mut self, status: bool, cx: &mut Context<Self>) {
|
||||
self.finding = status;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn clear_search_results(&mut self, cx: &mut Context<Self>) {
|
||||
self.local_result.update(cx, |this, cx| {
|
||||
*this = None;
|
||||
cx.notify();
|
||||
});
|
||||
self.global_result.update(cx, |this, cx| {
|
||||
*this = None;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
fn push_room(&mut self, id: u64, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(result) = self.global_result.read(cx).as_ref() {
|
||||
if let Some(room) = result.iter().find(|this| this.read(cx).id == id).cloned() {
|
||||
ChatRegistry::global(cx).update(cx, |this, cx| {
|
||||
this.push_room(room, cx);
|
||||
});
|
||||
window.dispatch_action(
|
||||
Box::new(AddPanel::new(PanelKind::Room(id), DockPlacement::Center)),
|
||||
cx,
|
||||
);
|
||||
self.clear_search_results(cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render_skeleton(&self, total: i32) -> impl IntoIterator<Item = impl IntoElement> {
|
||||
@@ -140,10 +345,34 @@ impl Sidebar {
|
||||
})
|
||||
}
|
||||
|
||||
fn render_items(rooms: &Vec<&Entity<Room>>, cx: &Context<Self>) -> Vec<FolderItem> {
|
||||
fn render_global_items(rooms: &[Entity<Room>], cx: &Context<Self>) -> Vec<FolderItem> {
|
||||
let mut items = Vec::with_capacity(rooms.len());
|
||||
|
||||
for room in rooms {
|
||||
for room in rooms.iter() {
|
||||
let this = room.read(cx);
|
||||
let id = this.id;
|
||||
let label = this.display_name(cx);
|
||||
let img = this.display_image(cx).map(img);
|
||||
|
||||
let item = FolderItem::new(id as usize)
|
||||
.label(label)
|
||||
.img(img)
|
||||
.on_click({
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.push_room(id, window, cx);
|
||||
})
|
||||
});
|
||||
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
items
|
||||
}
|
||||
|
||||
fn render_items(rooms: &[Entity<Room>], cx: &Context<Self>) -> Vec<FolderItem> {
|
||||
let mut items = Vec::with_capacity(rooms.len());
|
||||
|
||||
for room in rooms.iter() {
|
||||
let room = room.read(cx);
|
||||
let id = room.id;
|
||||
let ago = room.ago();
|
||||
@@ -198,30 +427,33 @@ impl Focusable for Sidebar {
|
||||
|
||||
impl Render for Sidebar {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let account = Account::global(cx).read(cx).profile.as_ref();
|
||||
let registry = ChatRegistry::global(cx).read(cx);
|
||||
let account = Account::get_global(cx).profile_ref();
|
||||
let registry = ChatRegistry::get_global(cx);
|
||||
|
||||
// Get all rooms
|
||||
let rooms = registry.rooms(cx);
|
||||
let loading = registry.loading();
|
||||
let loading = registry.loading;
|
||||
|
||||
// Get search result
|
||||
let local_result = self.local_result.read(cx);
|
||||
let global_result = self.global_result.read(cx);
|
||||
|
||||
div()
|
||||
.id("sidebar")
|
||||
.track_focus(&self.focus_handle)
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.on_action(cx.listener(Self::on_logout))
|
||||
.overflow_y_scroll()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.pt_1()
|
||||
.px_2()
|
||||
.pb_2()
|
||||
.py_1()
|
||||
.when_some(account, |this, profile| {
|
||||
this.child(
|
||||
div()
|
||||
.px_3()
|
||||
.h_7()
|
||||
.px_1p5()
|
||||
.flex_none()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
@@ -236,74 +468,81 @@ impl Render for Sidebar {
|
||||
.child(profile.shared_name()),
|
||||
)
|
||||
.child(
|
||||
Button::new("user_dropdown")
|
||||
.icon(IconName::Ellipsis)
|
||||
.small()
|
||||
.ghost()
|
||||
.popup_menu(|this, _window, _cx| {
|
||||
this.menu(
|
||||
"Profile",
|
||||
Box::new(ToggleModal {
|
||||
modal: ModalKind::Profile,
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
Button::new("user")
|
||||
.icon(IconName::Ellipsis)
|
||||
.small()
|
||||
.ghost()
|
||||
.rounded(ButtonRounded::Full)
|
||||
.popup_menu(|this, _window, _cx| {
|
||||
this.menu(
|
||||
"Profile",
|
||||
Box::new(ToggleModal {
|
||||
modal: ModalKind::Profile,
|
||||
}),
|
||||
)
|
||||
.menu(
|
||||
"Relays",
|
||||
Box::new(ToggleModal {
|
||||
modal: ModalKind::Relay,
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.menu(
|
||||
"Relays",
|
||||
Box::new(ToggleModal {
|
||||
modal: ModalKind::Relay,
|
||||
}),
|
||||
)
|
||||
.separator()
|
||||
.menu("Logout", Box::new(Logout))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("compose")
|
||||
.icon(IconName::PlusFill)
|
||||
.tooltip("Create DM or Group DM")
|
||||
.small()
|
||||
.primary()
|
||||
.rounded(ButtonRounded::Full)
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(ToggleModal {
|
||||
modal: ModalKind::Compose,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.px_3()
|
||||
.h_7()
|
||||
.flex_none()
|
||||
.child(self.find_input.clone()),
|
||||
)
|
||||
.when_some(global_result.as_ref(), |this, rooms| {
|
||||
this.child(
|
||||
div()
|
||||
.px_1()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.children(Self::render_global_items(rooms, cx)),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.px_1()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.text_sm()
|
||||
.font_medium()
|
||||
.child(
|
||||
SidebarButton::new("Find")
|
||||
.icon(IconName::Search)
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(ToggleModal {
|
||||
modal: ModalKind::Search,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
SidebarButton::new("New Chat")
|
||||
.icon(IconName::PlusCircleFill)
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(ToggleModal {
|
||||
modal: ModalKind::Compose,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.mb_1()
|
||||
.px_2()
|
||||
.flex()
|
||||
.justify_between()
|
||||
.items_center()
|
||||
.text_xs()
|
||||
.text_sm()
|
||||
.font_semibold()
|
||||
.text_color(cx.theme().text_placeholder)
|
||||
.child("Messages")
|
||||
@@ -326,19 +565,20 @@ impl Render for Sidebar {
|
||||
.active(cx.theme().ghost_element_background),
|
||||
)
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.split_into_folders(cx);
|
||||
this.toggle_folder(cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
.when(loading, |this| this.children(self.render_skeleton(6)))
|
||||
.map(|this| {
|
||||
if loading {
|
||||
this.children(self.render_skeleton(6))
|
||||
if let Some(rooms) = local_result {
|
||||
this.children(Self::render_items(rooms, cx))
|
||||
} else if !self.split_into_folders {
|
||||
let rooms: Vec<_> = rooms
|
||||
let rooms = rooms
|
||||
.values()
|
||||
.flat_map(|v| v.iter().cloned())
|
||||
.sorted_by_key(|e| Reverse(e.read(cx).created_at))
|
||||
.collect();
|
||||
.collect_vec();
|
||||
|
||||
this.children(Self::render_items(&rooms, cx))
|
||||
} else {
|
||||
|
||||
@@ -53,11 +53,6 @@ impl Subject {
|
||||
let registry = ChatRegistry::global(cx).read(cx);
|
||||
let subject = self.input.read(cx).text();
|
||||
|
||||
if subject.is_empty() {
|
||||
window.push_notification("Subject cannot be empty", cx);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(room) = registry.room(&self.id, cx) {
|
||||
room.update(cx, |this, cx| {
|
||||
this.subject = Some(subject);
|
||||
|
||||
Reference in New Issue
Block a user