From 107fedeafdba545418aec1903c46dd8d20dea897 Mon Sep 17 00:00:00 2001 From: reya <123083837+reyamir@users.noreply.github.com> Date: Sat, 26 Apr 2025 08:39:52 +0700 Subject: [PATCH] feat: Emoji Picker (#18) * wip: emoji picker * update --- Cargo.lock | 10 +++ Cargo.toml | 1 + assets/icons/emoji-fill.svg | 3 + crates/chats/src/room.rs | 28 +++++++ crates/coop/src/views/chat.rs | 34 ++++++--- crates/ui/Cargo.toml | 1 + crates/ui/src/button.rs | 2 +- crates/ui/src/divider.rs | 3 +- crates/ui/src/emoji_picker.rs | 136 ++++++++++++++++++++++++++++++++++ crates/ui/src/icon.rs | 2 + crates/ui/src/lib.rs | 1 + crates/ui/src/popover.rs | 30 +++++++- crates/ui/src/styled.rs | 2 +- 13 files changed, 238 insertions(+), 15 deletions(-) create mode 100644 assets/icons/emoji-fill.svg create mode 100644 crates/ui/src/emoji_picker.rs diff --git a/Cargo.lock b/Cargo.lock index 35d9eae..5aef228 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1684,6 +1684,15 @@ dependencies = [ "winreg", ] +[[package]] +name = "emojis" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e1f1df1f181f2539bac8bf027d31ca5ffbf9e559e3f2d09413b9107b5c02f4" +dependencies = [ + "phf", +] + [[package]] name = "encoding_rs" version = "0.8.35" @@ -6449,6 +6458,7 @@ dependencies = [ "anyhow", "chrono", "common", + "emojis", "gpui", "image", "itertools 0.13.0", diff --git a/Cargo.toml b/Cargo.toml index dee4a6e..d826110 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ nostr-sdk = { git = "https://github.com/rust-nostr/nostr", features = [ ] } # Others +emojis = "0.6.4" smol = "2" oneshot = "0.1.10" serde = { version = "1.0", features = ["derive"] } diff --git a/assets/icons/emoji-fill.svg b/assets/icons/emoji-fill.svg new file mode 100644 index 0000000..974ccf4 --- /dev/null +++ b/assets/icons/emoji-fill.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/chats/src/room.rs b/crates/chats/src/room.rs index e7bb896..1ba41de 100644 --- a/crates/chats/src/room.rs +++ b/crates/chats/src/room.rs @@ -40,6 +40,8 @@ pub struct Room { pub created_at: Timestamp, /// Subject of the room pub subject: Option, + /// Picture of the room + pub picture: Option, /// All members of the room pub members: Arc>, /// Kind @@ -82,10 +84,18 @@ impl Room { None }; + // Get the picture from the event's tags + let picture = if let Some(tag) = event.tags.find(TagKind::custom("picture")) { + tag.content().map(|s| s.to_owned().into()) + } else { + None + }; + Self { id, created_at, subject, + picture, members, kind: RoomKind::Unknown, } @@ -303,6 +313,17 @@ impl Room { cx.notify(); } + /// Updates the picture of the room + /// + /// # Arguments + /// + /// * `picture` - The new subject to set + /// * `cx` - The context to notify about the update + pub fn picture(&mut self, picture: String, cx: &mut Context) { + self.picture = Some(picture.into()); + cx.notify(); + } + /// Fetches metadata for all members in the room /// /// # Arguments @@ -380,6 +401,7 @@ impl Room { let public_key = profile.public_key(); let subject = self.subject.clone(); + let picture = self.picture.clone(); let pubkeys = self.members.clone(); let (tx, rx) = smol::channel::bounded::(pubkeys.len()); @@ -398,12 +420,18 @@ impl Room { }) .collect(); + // Add subject tag if it's present if let Some(subject) = subject { tags.push(Tag::from_standardized(TagStandard::Subject( subject.to_string(), ))); } + // Add picture tag if it's present + if let Some(picture) = picture { + tags.push(Tag::custom(TagKind::custom("picture"), vec![picture])); + } + for pubkey in pubkeys.iter() { match client .send_private_msg(*pubkey, &content, tags.clone()) diff --git a/crates/coop/src/views/chat.rs b/crates/coop/src/views/chat.rs index 1981937..37b416e 100644 --- a/crates/coop/src/views/chat.rs +++ b/crates/coop/src/views/chat.rs @@ -22,6 +22,7 @@ use std::{collections::HashMap, sync::Arc}; use ui::{ button::{Button, ButtonVariants}, dock_area::panel::{Panel, PanelEvent}, + emoji_picker::EmojiPicker, input::{InputEvent, TextInput}, notification::Notification, popup_menu::PopupMenu, @@ -594,16 +595,31 @@ impl Render for Chat { .w_full() .flex() .items_center() - .gap_2() + .gap_2p5() .child( - Button::new("upload") - .icon(Icon::new(IconName::Upload)) - .ghost() - .on_click(cx.listener(move |this, _, window, cx| { - this.upload_media(window, cx); - })) - .disabled(self.is_uploading) - .loading(self.is_uploading), + div() + .flex() + .items_center() + .gap_1() + .text_color( + cx.theme().base.step(cx, ColorScaleStep::ELEVEN), + ) + .child( + Button::new("upload") + .icon(Icon::new(IconName::Upload)) + .ghost() + .disabled(self.is_uploading) + .loading(self.is_uploading) + .on_click(cx.listener( + move |this, _, window, cx| { + this.upload_media(window, cx); + }, + )), + ) + .child( + EmojiPicker::new(self.input.downgrade()) + .icon(IconName::EmojiFill), + ), ) .child(self.input.clone()), ), diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 0e94842..7429d5b 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -24,3 +24,4 @@ uuid = "1.10" once_cell = "1.19.0" image = "0.25.1" linkify = "0.10.0" +emojis.workspace = true diff --git a/crates/ui/src/button.rs b/crates/ui/src/button.rs index ee0115c..4763a1f 100644 --- a/crates/ui/src/button.rs +++ b/crates/ui/src/button.rs @@ -348,7 +348,7 @@ impl RenderOnce for Button { Size::Size(px) => this.size(px), Size::XSmall => this.size_5(), Size::Small => this.size_6(), - _ => this.size_8(), + _ => this.size_9(), } } else { // Normal Button diff --git a/crates/ui/src/divider.rs b/crates/ui/src/divider.rs index fde513c..f096144 100644 --- a/crates/ui/src/divider.rs +++ b/crates/ui/src/divider.rs @@ -1,9 +1,10 @@ -use crate::theme::{scale::ColorScaleStep, ActiveTheme}; use gpui::{ div, prelude::FluentBuilder as _, px, Axis, Div, Hsla, IntoElement, ParentElement, RenderOnce, SharedString, Styled, }; +use crate::theme::{scale::ColorScaleStep, ActiveTheme}; + /// A divider that can be either vertical or horizontal. #[derive(IntoElement)] pub struct Divider { diff --git a/crates/ui/src/emoji_picker.rs b/crates/ui/src/emoji_picker.rs new file mode 100644 index 0000000..8c874fe --- /dev/null +++ b/crates/ui/src/emoji_picker.rs @@ -0,0 +1,136 @@ +use std::rc::Rc; + +use gpui::{ + div, impl_internal_actions, prelude::FluentBuilder, px, App, AppContext, Corner, Element, + InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString, + StatefulInteractiveElement, Styled, WeakEntity, Window, +}; +use serde::Deserialize; + +use crate::{ + button::{Button, ButtonVariants}, + input::TextInput, + popover::{Popover, PopoverContent}, + theme::{scale::ColorScaleStep, ActiveTheme}, + Icon, +}; + +#[derive(PartialEq, Clone, Debug, Deserialize)] +pub struct EmitEmoji(pub SharedString); + +impl_internal_actions!(emoji, [EmitEmoji]); + +#[derive(IntoElement)] +pub struct EmojiPicker { + icon: Option, + anchor: Option, + input: WeakEntity, + emojis: Rc>, +} + +impl EmojiPicker { + pub fn new(input: WeakEntity) -> Self { + let mut emojis: Vec = vec![]; + + emojis.extend( + emojis::Group::SmileysAndEmotion + .emojis() + .map(|e| SharedString::from(e.as_str())) + .collect::>(), + ); + + emojis.extend( + emojis::Group::Symbols + .emojis() + .map(|e| SharedString::from(e.as_str())) + .collect::>(), + ); + + Self { + input, + emojis: emojis.into(), + anchor: None, + icon: None, + } + } + + pub fn icon(mut self, icon: impl Into) -> Self { + self.icon = Some(icon.into()); + self + } + + pub fn anchor(mut self, corner: Corner) -> Self { + self.anchor = Some(corner); + self + } +} + +impl RenderOnce for EmojiPicker { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + Popover::new("emoji-picker") + .map(|this| { + if let Some(corner) = self.anchor { + this.anchor(corner) + } else { + this.anchor(gpui::Corner::BottomLeft) + } + }) + .trigger( + Button::new("emoji-trigger") + .when_some(self.icon, |this, icon| this.icon(icon)) + .ghost(), + ) + .content(move |window, cx| { + let emojis = self.emojis.clone(); + let input = self.input.clone(); + + cx.new(|cx| { + PopoverContent::new(window, cx, move |_window, cx| { + div() + .flex() + .flex_wrap() + .items_center() + .gap_2() + .children(emojis.iter().map(|e| { + div() + .id(e.clone()) + .flex_auto() + .size_10() + .flex() + .items_center() + .justify_center() + .rounded(px(cx.theme().radius)) + .child(e.clone()) + .hover(|this| { + this.bg(cx.theme().base.step(cx, ColorScaleStep::THREE)) + }) + .on_click({ + let item = e.clone(); + let input = input.upgrade(); + + move |_, window, cx| { + if let Some(input) = input.as_ref() { + input.update(cx, |this, cx| { + let current = this.text(); + let new_text = if current.is_empty() { + format!("{}", item) + } else if current.ends_with(" ") { + format!("{}{}", current, item) + } else { + format!("{} {}", current, item) + }; + this.set_text(new_text, window, cx); + }); + } + } + }) + })) + .into_any() + }) + .scrollable() + .max_h(px(300.)) + .max_w(px(300.)) + }) + }) + } +} diff --git a/crates/ui/src/icon.rs b/crates/ui/src/icon.rs index ad78091..3f631be 100644 --- a/crates/ui/src/icon.rs +++ b/crates/ui/src/icon.rs @@ -33,6 +33,7 @@ pub enum IconName { Ellipsis, Eye, EyeOff, + EmojiFill, Folder, FolderFill, Inbox, @@ -96,6 +97,7 @@ impl IconName { Self::EditFill => "icons/edit-fill.svg", Self::Ellipsis => "icons/ellipsis.svg", Self::Eye => "icons/eye.svg", + Self::EmojiFill => "icons/emoji-fill.svg", Self::EyeOff => "icons/eye-off.svg", Self::Folder => "icons/folder.svg", Self::FolderFill => "icons/folder-fill.svg", diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index e6de94f..ef4005a 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -7,6 +7,7 @@ pub mod context_menu; pub mod divider; pub mod dock_area; pub mod dropdown; +pub mod emoji_picker; pub mod history; pub mod indicator; pub mod input; diff --git a/crates/ui/src/popover.rs b/crates/ui/src/popover.rs index 273a2a1..2be97b0 100644 --- a/crates/ui/src/popover.rs +++ b/crates/ui/src/popover.rs @@ -1,12 +1,14 @@ -use crate::{Selectable, StyledExt as _}; +use std::{cell::RefCell, rc::Rc}; + use gpui::{ actions, anchored, deferred, div, prelude::FluentBuilder as _, px, AnyElement, App, Bounds, Context, Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, EventEmitter, FocusHandle, Focusable, GlobalElementId, Hitbox, InteractiveElement as _, IntoElement, KeyBinding, LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, - Render, Style, StyleRefinement, Styled, Window, + Render, ScrollHandle, StatefulInteractiveElement, Style, StyleRefinement, Styled, Window, }; -use std::{cell::RefCell, rc::Rc}; + +use crate::{Selectable, StyledExt as _}; const CONTEXT: &str = "Popover"; @@ -20,7 +22,10 @@ type PopoverChild = Rc) -> AnyElement>; pub struct PopoverContent { focus_handle: FocusHandle, + scroll_handle: ScrollHandle, max_width: Option, + max_height: Option, + scrollable: bool, child: PopoverChild, } @@ -30,11 +35,15 @@ impl PopoverContent { B: Fn(&mut Window, &mut Context) -> AnyElement + 'static, { let focus_handle = cx.focus_handle(); + let scroll_handle = ScrollHandle::default(); Self { focus_handle, + scroll_handle, child: Rc::new(content), max_width: None, + max_height: None, + scrollable: false, } } @@ -42,6 +51,16 @@ impl PopoverContent { self.max_width = Some(max_width); self } + + pub fn max_h(mut self, max_height: Pixels) -> Self { + self.max_height = Some(max_height); + self + } + + pub fn scrollable(mut self) -> Self { + self.scrollable = true; + self + } } impl EventEmitter for PopoverContent {} @@ -55,11 +74,16 @@ impl Focusable for PopoverContent { impl Render for PopoverContent { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { div() + .id("popup-content") .track_focus(&self.focus_handle) .key_context(CONTEXT) .on_action(cx.listener(|_, _: &Escape, _, cx| cx.emit(DismissEvent))) .p_2() + .when(self.scrollable, |this| { + this.overflow_y_scroll().track_scroll(&self.scroll_handle) + }) .when_some(self.max_width, |this, v| this.max_w(v)) + .when_some(self.max_height, |this, v| this.max_h(v)) .child(self.child.clone()(window, cx)) } } diff --git a/crates/ui/src/styled.rs b/crates/ui/src/styled.rs index 2e40986..a97e8de 100644 --- a/crates/ui/src/styled.rs +++ b/crates/ui/src/styled.rs @@ -64,7 +64,7 @@ pub trait StyledExt: Styled + Sized { /// Set as Popover style fn popover_style(self, cx: &mut App) -> Self { - self.bg(cx.theme().background) + self.bg(cx.theme().base.step(cx, ColorScaleStep::TWO)) .border_1() .border_color(cx.theme().base.step(cx, ColorScaleStep::SIX)) .shadow_lg()