feat: Emoji Picker (#18)

* wip: emoji picker

* update
This commit is contained in:
reya
2025-04-26 08:39:52 +07:00
committed by GitHub
parent 17251be3fd
commit 107fedeafd
13 changed files with 238 additions and 15 deletions

View File

@@ -40,6 +40,8 @@ pub struct Room {
pub created_at: Timestamp,
/// Subject of the room
pub subject: Option<SharedString>,
/// Picture of the room
pub picture: Option<SharedString>,
/// All members of the room
pub members: Arc<Vec<PublicKey>>,
/// 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>) {
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::<SendStatus>(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())

View File

@@ -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()),
),

View File

@@ -24,3 +24,4 @@ uuid = "1.10"
once_cell = "1.19.0"
image = "0.25.1"
linkify = "0.10.0"
emojis.workspace = true

View File

@@ -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

View File

@@ -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 {

View File

@@ -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<Icon>,
anchor: Option<Corner>,
input: WeakEntity<TextInput>,
emojis: Rc<Vec<SharedString>>,
}
impl EmojiPicker {
pub fn new(input: WeakEntity<TextInput>) -> Self {
let mut emojis: Vec<SharedString> = vec![];
emojis.extend(
emojis::Group::SmileysAndEmotion
.emojis()
.map(|e| SharedString::from(e.as_str()))
.collect::<Vec<SharedString>>(),
);
emojis.extend(
emojis::Group::Symbols
.emojis()
.map(|e| SharedString::from(e.as_str()))
.collect::<Vec<SharedString>>(),
);
Self {
input,
emojis: emojis.into(),
anchor: None,
icon: None,
}
}
pub fn icon(mut self, icon: impl Into<Icon>) -> 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.))
})
})
}
}

View File

@@ -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",

View File

@@ -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;

View File

@@ -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<T> = Rc<dyn Fn(&mut Window, &mut Context<T>) -> AnyElement>;
pub struct PopoverContent {
focus_handle: FocusHandle,
scroll_handle: ScrollHandle,
max_width: Option<Pixels>,
max_height: Option<Pixels>,
scrollable: bool,
child: PopoverChild<Self>,
}
@@ -30,11 +35,15 @@ impl PopoverContent {
B: Fn(&mut Window, &mut Context<Self>) -> 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<DismissEvent> for PopoverContent {}
@@ -55,11 +74,16 @@ impl Focusable for PopoverContent {
impl Render for PopoverContent {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> 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))
}
}

View File

@@ -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()