From 4e24061817c46fb1d78eb8faadcef1355d7a32e1 Mon Sep 17 00:00:00 2001 From: reya <123083837+reyamir@users.noreply.github.com> Date: Mon, 12 May 2025 20:46:01 +0700 Subject: [PATCH] feat: Redesign New Chat (#31) * make subject is optional * redesign * search * fix * adjust --- Cargo.lock | 76 ++--- Cargo.toml | 2 +- assets/icons/plus-fill.svg | 3 + crates/account/src/lib.rs | 9 + crates/chats/Cargo.toml | 2 + crates/chats/src/lib.rs | 98 ++++-- crates/chats/src/room.rs | 43 +-- crates/common/Cargo.toml | 3 +- crates/common/src/debounced_delay.rs | 55 +++ crates/common/src/lib.rs | 16 +- crates/coop/Cargo.toml | 2 +- crates/coop/src/chatspace.rs | 19 +- crates/coop/src/views/chat.rs | 35 +- crates/coop/src/views/compose.rs | 17 +- crates/coop/src/views/mod.rs | 1 - crates/coop/src/views/profile.rs | 2 +- crates/coop/src/views/sidebar/button.rs | 58 ---- crates/coop/src/views/sidebar/folder.rs | 2 +- crates/coop/src/views/sidebar/mod.rs | 436 ++++++++++++++++++------ crates/coop/src/views/subject.rs | 5 - crates/theme/src/colors.rs | 2 + crates/theme/src/lib.rs | 6 +- crates/theme/src/scale.rs | 2 + crates/ui/src/button.rs | 12 +- crates/ui/src/emoji_picker.rs | 15 +- crates/ui/src/icon.rs | 2 + crates/ui/src/input/input.rs | 6 +- crates/ui/src/list/list.rs | 4 +- crates/ui/src/list/list_item.rs | 4 +- crates/ui/src/popup_menu.rs | 2 +- crates/ui/src/skeleton.rs | 2 +- crates/ui/src/tooltip.rs | 6 +- 32 files changed, 580 insertions(+), 367 deletions(-) create mode 100644 assets/icons/plus-fill.svg create mode 100644 crates/common/src/debounced_delay.rs delete mode 100644 crates/coop/src/views/sidebar/button.rs diff --git a/Cargo.lock b/Cargo.lock index 1befafb..b55daa9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -966,6 +966,7 @@ dependencies = [ "anyhow", "chrono", "common", + "fuzzy-matcher", "global", "gpui", "itertools 0.13.0", @@ -1136,7 +1137,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24" +source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3" dependencies = [ "indexmap", "rustc-hash 2.1.1", @@ -1171,13 +1172,14 @@ version = "0.1.5" dependencies = [ "anyhow", "chrono", + "futures", "global", "gpui", "itertools 0.13.0", "nostr-sdk", "qrcode-generator", - "random_name_generator", "smallvec", + "smol", ] [[package]] @@ -1527,7 +1529,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24" +source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3" dependencies = [ "proc-macro2", "quote", @@ -2169,6 +2171,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -2318,7 +2329,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24" +source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2410,7 +2421,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24" +source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3" dependencies = [ "proc-macro2", "quote", @@ -2634,7 +2645,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24" +source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3" dependencies = [ "anyhow", "bytes", @@ -2651,7 +2662,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24" +source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3" dependencies = [ "rustls", "rustls-platform-verifier", @@ -3109,12 +3120,6 @@ dependencies = [ "libc", ] -[[package]] -name = "joinery" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72167d68f5fce3b8655487b8038691a3c9984ee769590f93f2a631f4ad64e4f5" - [[package]] name = "jpeg-decoder" version = "0.3.1" @@ -3392,7 +3397,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24" +source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3" dependencies = [ "anyhow", "bindgen 0.71.1", @@ -4699,23 +4704,6 @@ dependencies = [ "getrandom 0.3.2", ] -[[package]] -name = "random_name_generator" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83f35cf4ff1039c849a4d890c6aa4332df47f9def1e9398ef1e5959bc7f89992" -dependencies = [ - "anyhow", - "bitflags 2.9.0", - "clap", - "lazy_static", - "log", - "rand 0.8.5", - "regex", - "rust-embed", - "titlecase", -] - [[package]] name = "rangemap" version = "1.5.1" @@ -4843,7 +4831,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24" +source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3" dependencies = [ "derive_refineable", "workspace-hack", @@ -4982,7 +4970,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24" +source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3" dependencies = [ "anyhow", "bytes", @@ -5168,11 +5156,12 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ "web-time", + "zeroize", ] [[package]] @@ -5452,7 +5441,7 @@ checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" [[package]] name = "semantic_version" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24" +source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3" dependencies = [ "anyhow", "serde", @@ -5775,7 +5764,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24" +source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3" dependencies = [ "arrayvec", "log", @@ -6196,17 +6185,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "titlecase" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38397a8cdb017cfeb48bf6c154d6de975ac69ffeed35980fde199d2ee0842042" -dependencies = [ - "joinery", - "lazy_static", - "regex", -] - [[package]] name = "tokio" version = "1.45.0" @@ -6695,7 +6673,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#ab3e5cdc6cf416181fe009d13476ed5e779f7c24" +source = "git+https://github.com/zed-industries/zed#d6c7cdd60ff3cbede868b7c73d92ce808f7b9ee3" dependencies = [ "anyhow", "async-fs", diff --git a/Cargo.toml b/Cargo.toml index d0678d7..5feb624 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,12 +24,12 @@ nostr-keyring = { git = "https://github.com/rust-nostr/nostr" } # Others emojis = "0.6.4" smol = "2" +futures = "0.3" oneshot = "0.1.10" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" dirs = "5.0" itertools = "0.13.0" -futures = "0.3.30" chrono = "0.4.38" tracing = "0.1.40" anyhow = "1.0.44" diff --git a/assets/icons/plus-fill.svg b/assets/icons/plus-fill.svg new file mode 100644 index 0000000..1ba3086 --- /dev/null +++ b/assets/icons/plus-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/account/src/lib.rs b/crates/account/src/lib.rs index 700015a..f62ec37 100644 --- a/crates/account/src/lib.rs +++ b/crates/account/src/lib.rs @@ -27,6 +27,10 @@ impl Account { cx.global::().0.clone() } + pub fn get_global(cx: &App) -> &Self { + cx.global::().0.read(cx) + } + pub fn set_global(account: Entity, cx: &mut App) { cx.set_global(GlobalAccount(account)); } @@ -162,6 +166,11 @@ impl Account { .detach(); } + /// Get the reference to profile. + pub fn profile_ref(&self) -> Option<&Profile> { + self.profile.as_ref() + } + /// Sets the profile for the account. pub fn profile(&mut self, profile: Profile, cx: &mut Context) { self.profile = Some(profile); diff --git a/crates/chats/Cargo.toml b/crates/chats/Cargo.toml index 990edec..a60a71a 100644 --- a/crates/chats/Cargo.toml +++ b/crates/chats/Cargo.toml @@ -20,3 +20,5 @@ smallvec.workspace = true smol.workspace = true oneshot.workspace = true log.workspace = true + +fuzzy-matcher = "0.3.7" diff --git a/crates/chats/src/lib.rs b/crates/chats/src/lib.rs index b0fea62..18474c7 100644 --- a/crates/chats/src/lib.rs +++ b/crates/chats/src/lib.rs @@ -1,7 +1,11 @@ -use std::{cmp::Reverse, collections::HashMap}; +use std::{ + cmp::Reverse, + collections::{BTreeMap, BTreeSet, HashMap}, +}; use anyhow::Error; use common::room_hash; +use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; use global::get_client; use gpui::{App, AppContext, Context, Entity, Global, Subscription, Task, Window}; use itertools::Itertools; @@ -33,14 +37,11 @@ impl Global for GlobalChatRegistry {} /// - Handling messages and room creation pub struct ChatRegistry { /// Collection of all chat rooms - rooms: Vec>, - + rooms: BTreeSet>, /// Map of user public keys to their profile metadata - profiles: Entity>>, - + profiles: Entity>>, /// Indicates if rooms are currently being loaded - loading: bool, - + pub loading: bool, /// Subscriptions for observing changes #[allow(dead_code)] subscriptions: SmallVec<[Subscription; 1]>, @@ -52,6 +53,11 @@ impl ChatRegistry { cx.global::().0.clone() } + /// Retrieve the ChatRegistry instance + pub fn get_global(cx: &App) -> &Self { + cx.global::().0.read(cx) + } + /// Set the global ChatRegistry instance pub fn set_global(state: Entity, cx: &mut App) { cx.set_global(GlobalChatRegistry(state)); @@ -59,7 +65,7 @@ impl ChatRegistry { /// Create a new ChatRegistry instance fn new(cx: &mut Context) -> Self { - let profiles = cx.new(|_| HashMap::new()); + let profiles = cx.new(|_| BTreeMap::new()); let mut subscriptions = smallvec![]; // Observe new Room creations to collect profile metadata @@ -82,18 +88,13 @@ impl ChatRegistry { })); Self { - rooms: vec![], + rooms: BTreeSet::new(), loading: true, profiles, subscriptions, } } - /// Get the global loading status - pub fn loading(&self) -> bool { - self.loading - } - /// Get a room by its ID. pub fn room(&self, id: &u64, cx: &App) -> Option> { self.rooms @@ -103,33 +104,43 @@ impl ChatRegistry { } /// Get all rooms grouped by their kind. - pub fn rooms(&self, cx: &App) -> HashMap>> { - let mut groups = HashMap::new(); + pub fn rooms(&self, cx: &App) -> BTreeMap>> { + let mut groups = BTreeMap::new(); groups.insert(RoomKind::Ongoing, Vec::new()); groups.insert(RoomKind::Trusted, Vec::new()); groups.insert(RoomKind::Unknown, Vec::new()); for room in self.rooms.iter() { let kind = room.read(cx).kind; - groups.entry(kind).or_insert_with(Vec::new).push(room); + groups + .entry(kind) + .or_insert_with(Vec::new) + .push(room.to_owned()); } groups } - /// Get rooms by their kind. - pub fn rooms_by_kind(&self, kind: RoomKind, cx: &App) -> Vec<&Entity> { - self.rooms - .iter() - .filter(|room| room.read(cx).kind == kind) - .collect() - } - /// Get the IDs of all rooms. pub fn room_ids(&self, cx: &mut Context) -> Vec { self.rooms.iter().map(|room| room.read(cx).id).collect() } + /// Search rooms by their name. + pub fn search(&self, query: &str, cx: &App) -> Vec> { + let matcher = SkimMatcherV2::default(); + + self.rooms + .iter() + .filter(|room| { + matcher + .fuzzy_match(room.read(cx).display_name(cx).as_ref(), query) + .is_some() + }) + .cloned() + .collect() + } + /// Load all rooms from the lmdb. /// /// This method: @@ -215,7 +226,6 @@ impl ChatRegistry { .collect(); this.rooms.extend(rooms); - this.rooms.sort_by_key(|r| Reverse(r.read(cx).created_at)); this.loading = false; cx.notify(); @@ -259,15 +269,20 @@ impl ChatRegistry { Profile::new(*public_key, metadata) } - /// Add a new room to the registry + /// Parse a Nostr event into a Room and push it to the registry /// - /// Returns an error if the room already exists - pub fn push(&mut self, event: &Event, window: &mut Window, cx: &mut Context) -> u64 { + /// Returns the ID of the new room + pub fn push_event( + &mut self, + event: &Event, + window: &mut Window, + cx: &mut Context, + ) -> u64 { let room = Room::new(event).kind(RoomKind::Ongoing); let id = room.id; - if !self.rooms.iter().any(|r| r.read(cx) == &room) { - self.rooms.insert(0, cx.new(|_| room)); + if !self.rooms.iter().any(|this| this.read(cx) == &room) { + self.rooms.insert(cx.new(|_| room)); cx.notify(); } else { window.push_notification("Room already exists", cx); @@ -276,6 +291,20 @@ impl ChatRegistry { id } + /// Parse a nostr event into Room and push to the registry + /// + /// Returns the ID of the new room + pub fn push_room(&mut self, room: Entity, cx: &mut Context) -> u64 { + let id = room.read(cx).id; + + if !self.rooms.iter().any(|this| this.read(cx) == room.read(cx)) { + self.rooms.insert(room); + cx.notify(); + } + + id + } + /// Push a new message to a room /// /// If the room doesn't exist, it will be created. @@ -291,14 +320,9 @@ impl ChatRegistry { this.emit_message(event, window, cx); }); }); - - cx.defer_in(window, |this, _, cx| { - this.rooms - .sort_by_key(|room| Reverse(room.read(cx).created_at)); - }); } else { // Push the new room to the front of the list - self.rooms.insert(0, cx.new(|_| Room::new(&event))); + self.rooms.insert(cx.new(|_| Room::new(&event))); cx.notify(); } } diff --git a/crates/chats/src/room.rs b/crates/chats/src/room.rs index 1ba41de..f379c26 100644 --- a/crates/chats/src/room.rs +++ b/crates/chats/src/room.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{cmp::Ordering, sync::Arc}; use account::Account; use anyhow::Error; @@ -27,7 +27,7 @@ pub enum SendStatus { Failed(Error), } -#[derive(Clone, Copy, Hash, Debug, PartialEq, Eq, Default)] +#[derive(Clone, Copy, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default)] pub enum RoomKind { Ongoing, Trusted, @@ -35,6 +35,7 @@ pub enum RoomKind { Unknown, } +#[derive(Debug)] pub struct Room { pub id: u64, pub created_at: Timestamp, @@ -48,7 +49,19 @@ pub struct Room { pub kind: RoomKind, } -impl EventEmitter for Room {} +impl Ord for Room { + fn cmp(&self, other: &Self) -> Ordering { + self.created_at.cmp(&other.created_at) + } +} + +impl PartialOrd for Room { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Eq for Room {} impl PartialEq for Room { fn eq(&self, other: &Self) -> bool { @@ -56,6 +69,8 @@ impl PartialEq for Room { } } +impl EventEmitter for Room {} + impl Room { /// Creates a new Room instance from a Nostr event /// @@ -186,28 +201,6 @@ impl Room { } } - /// Gets all avatars for members in the room - /// - /// # Arguments - /// - /// * `cx` - The App context - /// - /// # Returns - /// - /// A vector of SharedString containing all members' avatars - pub fn avatars(&self, cx: &App) -> Vec { - let profiles: Vec = self - .members - .iter() - .map(|pubkey| ChatRegistry::global(cx).read(cx).profile(pubkey, cx)) - .collect(); - - profiles - .iter() - .map(|member| member.shared_avatar()) - .collect() - } - /// Gets a formatted string of member names /// /// # Arguments diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 2b3d50a..87387e1 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -13,6 +13,7 @@ anyhow.workspace = true itertools.workspace = true chrono.workspace = true smallvec.workspace = true +smol.workspace = true +futures.workspace = true -random_name_generator = "0.3.6" qrcode-generator = "5.0.0" diff --git a/crates/common/src/debounced_delay.rs b/crates/common/src/debounced_delay.rs new file mode 100644 index 0000000..03281d1 --- /dev/null +++ b/crates/common/src/debounced_delay.rs @@ -0,0 +1,55 @@ +use futures::{channel::oneshot, FutureExt}; +use gpui::{Context, Task}; +use std::{marker::PhantomData, time::Duration}; + +pub struct DebouncedDelay { + task: Option>, + cancel_channel: Option>, + _phantom_data: PhantomData, +} + +impl Default for DebouncedDelay { + fn default() -> Self { + Self::new() + } +} + +impl DebouncedDelay { + pub fn new() -> Self { + Self { + task: None, + cancel_channel: None, + _phantom_data: PhantomData, + } + } + + pub fn fire_new(&mut self, delay: Duration, cx: &mut Context, func: F) + where + F: 'static + Send + FnOnce(&mut E, &mut Context) -> Task<()>, + { + if let Some(channel) = self.cancel_channel.take() { + _ = channel.send(()); + } + + let (sender, mut receiver) = oneshot::channel::<()>(); + self.cancel_channel = Some(sender); + + let previous_task = self.task.take(); + self.task = Some(cx.spawn(async move |entity, cx| { + let mut timer = cx.background_executor().timer(delay).fuse(); + + if let Some(previous_task) = previous_task { + previous_task.await; + } + + futures::select_biased! { + _ = receiver => return, + _ = timer => {} + } + + if let Ok(task) = entity.update(cx, |project, cx| (func)(project, cx)) { + task.await; + } + })); + } +} diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 24d2623..14f5358 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -4,14 +4,13 @@ use std::{ sync::Arc, }; -use anyhow::Context; use global::constants::NIP96_SERVER; use gpui::Image; use itertools::Itertools; use nostr_sdk::prelude::*; use qrcode_generator::QrCodeEcc; -use rnglib::{Language, RNG}; +pub mod debounced_delay; pub mod profile; pub async fn nip96_upload(client: &Client, file: Vec) -> anyhow::Result { @@ -43,19 +42,6 @@ pub fn room_hash(event: &Event) -> u64 { hasher.finish() } -pub fn device_pubkey(event: &Event) -> Result { - let n_tag = event.tags.find(TagKind::custom("n")).context("Invalid")?; - let hex = n_tag.content().context("Invalid")?; - let pubkey = PublicKey::parse(hex)?; - - Ok(pubkey) -} - -pub fn random_name(length: usize) -> String { - let rng = RNG::from(&Language::Roman); - rng.generate_names(length, true).join("-").to_lowercase() -} - pub fn create_qr(data: &str) -> Result, anyhow::Error> { let qr = qrcode_generator::to_png_to_vec_from_str(data, QrCodeEcc::Medium, 256)?; let img = Arc::new(Image { diff --git a/crates/coop/Cargo.toml b/crates/coop/Cargo.toml index e41e68a..add2abe 100644 --- a/crates/coop/Cargo.toml +++ b/crates/coop/Cargo.toml @@ -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"] } diff --git a/crates/coop/src/chatspace.rs b/crates/coop/src/chatspace.rs index c87564a..4f15790 100644 --- a/crates/coop/src/chatspace.rs +++ b/crates/coop/src/chatspace.rs @@ -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() diff --git a/crates/coop/src/views/chat.rs b/crates/coop/src/views/chat.rs index 9f7a9cf..5903551 100644 --- a/crates/coop/src/views/chat.rs +++ b/crates/coop/src/views/chat.rs @@ -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; diff --git a/crates/coop/src/views/compose.rs b/crates/coop/src/views/compose.rs index f10651e..9e3e4f2 100644 --- a/crates/coop/src/views/compose.rs +++ b/crates/coop/src/views/compose.rs @@ -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> = 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()), ), ) diff --git a/crates/coop/src/views/mod.rs b/crates/coop/src/views/mod.rs index 88fd8a7..b5914f4 100644 --- a/crates/coop/src/views/mod.rs +++ b/crates/coop/src/views/mod.rs @@ -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; diff --git a/crates/coop/src/views/profile.rs b/crates/coop/src/views/profile.rs index 9211939..e6f25d1 100644 --- a/crates/coop/src/views/profile.rs +++ b/crates/coop/src/views/profile.rs @@ -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() diff --git a/crates/coop/src/views/sidebar/button.rs b/crates/coop/src/views/sidebar/button.rs deleted file mode 100644 index cb32847..0000000 --- a/crates/coop/src/views/sidebar/button.rs +++ /dev/null @@ -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; - -#[derive(IntoElement)] -pub struct SidebarButton { - base: Div, - label: SharedString, - icon: Option, - handler: Handler, -} - -impl SidebarButton { - pub fn new(label: impl Into) -> 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) -> 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)) - } -} diff --git a/crates/coop/src/views/sidebar/folder.rs b/crates/coop/src/views/sidebar/folder.rs index eb02718..53ac45c 100644 --- a/crates/coop/src/views/sidebar/folder.rs +++ b/crates/coop/src/views/sidebar/folder.rs @@ -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() diff --git a/crates/coop/src/views/sidebar/mod.rs b/crates/coop/src/views/sidebar/mod.rs index 94c26dc..14420e6 100644 --- a/crates/coop/src/views/sidebar/mod.rs +++ b/crates/coop/src/views/sidebar/mod.rs @@ -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::new(window, cx) @@ -52,11 +60,21 @@ pub enum SubItem { pub struct Sidebar { name: SharedString, + // Search + find_input: Entity, + find_debouncer: DebouncedDelay, + finding: bool, + local_result: Entity>>>, + global_result: Entity>>>, + // Layout split_into_folders: bool, active_items: HashSet, active_subitems: HashSet, + // 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 { + fn view(window: &mut Window, cx: &mut Context) -> 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) { + fn toggle_folder(&mut self, cx: &mut Context) { self.split_into_folders = !self.split_into_folders; cx.notify(); } - fn on_logout(&mut self, _: &Logout, window: &mut Window, cx: &mut Context) { - let task: Task> = cx.background_spawn(async move { - let client = get_client(); - _ = client.reset().await; + fn debounced_search(&self, cx: &mut Context) -> Task<()> { + cx.spawn(async move |this, cx| { + this.update(cx, |this, cx| { + this.search(cx); + }) + .ok(); + }) + } - Ok(()) + fn nip50_search(&self, cx: &App) -> Task, 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::(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) { + 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.finding = status; + cx.notify(); + } + + fn clear_search_results(&mut self, cx: &mut Context) { + 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) { + 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 { @@ -140,10 +345,34 @@ impl Sidebar { }) } - fn render_items(rooms: &Vec<&Entity>, cx: &Context) -> Vec { + fn render_global_items(rooms: &[Entity], cx: &Context) -> Vec { 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], cx: &Context) -> Vec { + 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) -> 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 { diff --git a/crates/coop/src/views/subject.rs b/crates/coop/src/views/subject.rs index f1e735b..2b114bd 100644 --- a/crates/coop/src/views/subject.rs +++ b/crates/coop/src/views/subject.rs @@ -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); diff --git a/crates/theme/src/colors.rs b/crates/theme/src/colors.rs index 4b8f366..1fe1609 100644 --- a/crates/theme/src/colors.rs +++ b/crates/theme/src/colors.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use gpui::{hsla, Hsla, Rgba}; use crate::scale::{ColorScale, ColorScaleSet, ColorScales}; diff --git a/crates/theme/src/lib.rs b/crates/theme/src/lib.rs index 9d77cb7..9454658 100644 --- a/crates/theme/src/lib.rs +++ b/crates/theme/src/lib.rs @@ -163,7 +163,7 @@ impl ThemeColor { ghost_element_hover: neutral().light_alpha().step_3(), ghost_element_active: neutral().light_alpha().step_4(), ghost_element_selected: neutral().light_alpha().step_5(), - ghost_element_disabled: neutral().light_alpha().step_3(), + ghost_element_disabled: neutral().light_alpha().step_2(), text: neutral().light().step_12(), text_muted: neutral().light().step_11(), text_placeholder: neutral().light().step_10(), @@ -202,7 +202,7 @@ impl ThemeColor { elevated_surface_background: neutral().dark().step_3(), surface_background: neutral().dark().step_2(), background: neutral().dark().step_1(), - element_foreground: brand().dark().step_12(), + element_foreground: brand().dark().step_1(), element_background: brand().dark().step_9(), element_hover: brand().dark_alpha().step_10(), element_active: brand().dark().step_10(), @@ -213,7 +213,7 @@ impl ThemeColor { ghost_element_hover: neutral().dark_alpha().step_3(), ghost_element_active: neutral().dark_alpha().step_4(), ghost_element_selected: neutral().dark_alpha().step_5(), - ghost_element_disabled: neutral().dark_alpha().step_3(), + ghost_element_disabled: neutral().dark_alpha().step_2(), text: neutral().dark().step_12(), text_muted: neutral().dark().step_11(), text_placeholder: neutral().dark().step_10(), diff --git a/crates/theme/src/scale.rs b/crates/theme/src/scale.rs index 1434b4f..85f8d7a 100644 --- a/crates/theme/src/scale.rs +++ b/crates/theme/src/scale.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use gpui::{Hsla, SharedString}; /// A collection of colors that are used to style the UI. diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index b07cf6f..c729326 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -308,9 +308,15 @@ impl RenderOnce for Button { // Normal Button match self.size { Size::Size(size) => this.px(size * 0.2), - Size::XSmall => this.h_6().px_1p5(), - Size::Small => this.h_7().px_2(), - Size::Large => this.h_10().px_3(), + Size::XSmall => this.h_6().px_2(), + Size::Small => { + if self.icon.is_some() { + this.h_7().pl_2().pr_3() + } else { + this.h_7().px_3() + } + } + Size::Large => this.h_10().px_4(), _ => this.h_9().px_2(), } } diff --git a/crates/ui/src/emoji_picker.rs b/crates/ui/src/emoji_picker.rs index 4fb5af5..ec2098a 100644 --- a/crates/ui/src/emoji_picker.rs +++ b/crates/ui/src/emoji_picker.rs @@ -24,12 +24,12 @@ impl_internal_actions!(emoji, [EmitEmoji]); pub struct EmojiPicker { icon: Option, anchor: Option, - input: WeakEntity, + target_input: WeakEntity, emojis: Rc>, } impl EmojiPicker { - pub fn new(input: WeakEntity) -> Self { + pub fn new(target_input: WeakEntity) -> Self { let mut emojis: Vec = vec![]; emojis.extend( @@ -39,15 +39,8 @@ impl EmojiPicker { .collect::>(), ); - emojis.extend( - emojis::Group::Symbols - .emojis() - .map(|e| SharedString::from(e.as_str())) - .collect::>(), - ); - Self { - input, + target_input, emojis: emojis.into(), anchor: None, icon: None, @@ -82,7 +75,7 @@ impl RenderOnce for EmojiPicker { ) .content(move |window, cx| { let emojis = self.emojis.clone(); - let input = self.input.clone(); + let input = self.target_input.clone(); cx.new(|cx| { PopoverContent::new(window, cx, move |_window, cx| { diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index c062933..a482b44 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -54,6 +54,7 @@ pub enum IconName { PanelRightClose, PanelRightOpen, Plus, + PlusFill, PlusCircleFill, Relays, ResizeCorner, @@ -124,6 +125,7 @@ impl IconName { Self::PanelRightClose => "icons/panel-right-close.svg", Self::PanelRightOpen => "icons/panel-right-open.svg", Self::Plus => "icons/plus.svg", + Self::PlusFill => "icons/plus-fill.svg", Self::PlusCircleFill => "icons/plus-circle-fill.svg", Self::Relays => "icons/relays.svg", Self::ResizeCorner => "icons/resize-corner.svg", diff --git a/crates/ui/src/input/input.rs b/crates/ui/src/input/input.rs index 5b02df3..eea560c 100644 --- a/crates/ui/src/input/input.rs +++ b/crates/ui/src/input/input.rs @@ -464,13 +464,13 @@ impl TextInput { } /// Set the disabled state of the input field. - pub fn set_disabled(&mut self, disabled: bool, _window: &mut Window, cx: &mut Context) { + pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context) { self.disabled = disabled; cx.notify(); } /// Set the masked state of the input field. - pub fn set_masked(&mut self, masked: bool, _window: &mut Window, cx: &mut Context) { + pub fn set_masked(&mut self, masked: bool, cx: &mut Context) { self.masked = masked; cx.notify(); } @@ -576,7 +576,7 @@ impl TextInput { } /// Set true to show indicator at the input right. - pub fn set_loading(&mut self, loading: bool, _window: &mut Window, cx: &mut Context) { + pub fn set_loading(&mut self, loading: bool, cx: &mut Context) { self.loading = loading; cx.notify(); } diff --git a/crates/ui/src/list/list.rs b/crates/ui/src/list/list.rs index 0bcf5c1..e8c4023 100644 --- a/crates/ui/src/list/list.rs +++ b/crates/ui/src/list/list.rs @@ -286,10 +286,10 @@ where } } - fn set_loading(&mut self, loading: bool, window: &mut Window, cx: &mut Context) { + fn set_loading(&mut self, loading: bool, _window: &mut Window, cx: &mut Context) { self.loading = loading; if let Some(input) = &self.query_input { - input.update(cx, |input, cx| input.set_loading(loading, window, cx)) + input.update(cx, |input, cx| input.set_loading(loading, cx)) } cx.notify(); } diff --git a/crates/ui/src/list/list_item.rs b/crates/ui/src/list/list_item.rs index 1f9925b..9f1a5b9 100644 --- a/crates/ui/src/list/list_item.rs +++ b/crates/ui/src/list/list_item.rs @@ -130,7 +130,7 @@ impl RenderOnce for ListItem { let is_active = self.selected || self.confirmed; self.base - .text_color(cx.theme().text_muted) + .text_color(cx.theme().text) .relative() .items_center() .justify_between() @@ -147,7 +147,7 @@ impl RenderOnce for ListItem { }) .when(is_active, |this| this.bg(cx.theme().element_active)) .when(!is_active && !self.disabled, |this| { - this.hover(|this| this.bg(cx.theme().surface_background)) + this.hover(|this| this.bg(cx.theme().elevated_surface_background)) }) // Mouse enter .when_some(self.on_mouse_enter, |this, on_mouse_enter| { diff --git a/crates/ui/src/popup_menu.rs b/crates/ui/src/popup_menu.rs index 7240c01..068086d 100644 --- a/crates/ui/src/popup_menu.rs +++ b/crates/ui/src/popup_menu.rs @@ -590,7 +590,7 @@ impl Render for PopupMenu { .h(px(1.)) .mx_neg_1() .my_0p5() - .bg(cx.theme().border_variant), + .bg(cx.theme().border_disabled), ) } PopupMenuItem::ElementItem { render, .. } => this diff --git a/crates/ui/src/skeleton.rs b/crates/ui/src/skeleton.rs index 7ae4d8f..327a4e1 100644 --- a/crates/ui/src/skeleton.rs +++ b/crates/ui/src/skeleton.rs @@ -35,7 +35,7 @@ impl RenderOnce for Skeleton { fn render(self, _window: &mut gpui::Window, cx: &mut gpui::App) -> impl IntoElement { div().child( self.base - .bg(cx.theme().ghost_element_disabled) + .bg(cx.theme().ghost_element_active) .with_animation( "skeleton", Animation::new(Duration::from_secs(2)) diff --git a/crates/ui/src/tooltip.rs b/crates/ui/src/tooltip.rs index c1fd6f2..d7b6cd0 100644 --- a/crates/ui/src/tooltip.rs +++ b/crates/ui/src/tooltip.rs @@ -23,11 +23,11 @@ impl Render for Tooltip { .p_2() .border_1() .border_color(cx.theme().border) - .bg(cx.theme().surface_background) - .shadow_lg() + .bg(cx.theme().background) + .shadow_md() .rounded_lg() .text_sm() - .text_color(cx.theme().text_muted) + .text_color(cx.theme().text) .line_height(relative(1.25)) .child(self.text.clone()), )