Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ui/src/components/conversation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::components::app::{CURRENT_ROOM, EDIT_ROOM_MODAL, MEMBER_INFO_MODAL, N
use crate::room_data::SendMessageError;
use crate::util::ecies::encrypt_with_symmetric_key;
use crate::util::{format_utc_as_full_datetime, format_utc_as_local_time, get_current_system_time};
mod emoji_picker;
mod message_actions;
mod message_input;
mod not_member_notification;
Expand Down
151 changes: 151 additions & 0 deletions ui/src/components/conversation/emoji_picker.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
//! Emoji picker component for inserting emojis into messages.

use dioxus::prelude::*;

/// Emoji categories with their emojis
pub const EMOJI_CATEGORIES: &[(&str, &str, &[&str])] = &[
(
"smileys",
"๐Ÿ˜€",
&[
"๐Ÿ˜€", "๐Ÿ˜ƒ", "๐Ÿ˜„", "๐Ÿ˜", "๐Ÿ˜†", "๐Ÿ˜…", "๐Ÿคฃ", "๐Ÿ˜‚", "๐Ÿ™‚", "๐Ÿ˜Š", "๐Ÿ˜‡", "๐Ÿฅฐ", "๐Ÿ˜", "๐Ÿคฉ",
"๐Ÿ˜˜", "๐Ÿ˜—", "๐Ÿ˜š", "๐Ÿ˜™", "๐Ÿฅฒ", "๐Ÿ˜‹", "๐Ÿ˜›", "๐Ÿ˜œ", "๐Ÿคช", "๐Ÿ˜", "๐Ÿค‘", "๐Ÿค—", "๐Ÿคญ", "๐Ÿคซ",
"๐Ÿค”", "๐Ÿค", "๐Ÿคจ", "๐Ÿ˜", "๐Ÿ˜‘", "๐Ÿ˜ถ", "๐Ÿ˜", "๐Ÿ˜’", "๐Ÿ™„", "๐Ÿ˜ฌ", "๐Ÿ˜ฎโ€๐Ÿ’จ", "๐Ÿคฅ", "๐Ÿ˜Œ", "๐Ÿ˜”",
"๐Ÿ˜ช", "๐Ÿคค", "๐Ÿ˜ด", "๐Ÿ˜ท", "๐Ÿค’", "๐Ÿค•", "๐Ÿคข", "๐Ÿคฎ", "๐Ÿคง", "๐Ÿฅต", "๐Ÿฅถ", "๐Ÿฅด", "๐Ÿ˜ต", "๐Ÿคฏ",
"๐Ÿค ", "๐Ÿฅณ", "๐Ÿฅธ", "๐Ÿ˜Ž", "๐Ÿค“", "๐Ÿง", "๐Ÿ˜•", "๐Ÿ˜Ÿ", "๐Ÿ™", "๐Ÿ˜ฎ", "๐Ÿ˜ฏ", "๐Ÿ˜ฒ", "๐Ÿ˜ณ", "๐Ÿฅบ",
"๐Ÿ˜ฆ", "๐Ÿ˜ง", "๐Ÿ˜จ", "๐Ÿ˜ฐ", "๐Ÿ˜ฅ", "๐Ÿ˜ข", "๐Ÿ˜ญ", "๐Ÿ˜ฑ", "๐Ÿ˜–", "๐Ÿ˜ฃ", "๐Ÿ˜ž", "๐Ÿ˜“", "๐Ÿ˜ฉ", "๐Ÿ˜ซ",
"๐Ÿฅฑ", "๐Ÿ˜ค", "๐Ÿ˜ก", "๐Ÿ˜ ", "๐Ÿคฌ", "๐Ÿ˜ˆ", "๐Ÿ‘ฟ", "๐Ÿ’€", "โ˜ ๏ธ", "๐Ÿ’ฉ", "๐Ÿคก", "๐Ÿ‘น", "๐Ÿ‘บ", "๐Ÿ‘ป",
"๐Ÿ‘ฝ", "๐Ÿ‘พ", "๐Ÿค–",
],
),
(
"gestures",
"๐Ÿ‘‹",
&[
"๐Ÿ‘‹", "๐Ÿคš", "๐Ÿ–๏ธ", "โœ‹", "๐Ÿ––", "๐Ÿ‘Œ", "๐ŸคŒ", "๐Ÿค", "โœŒ๏ธ", "๐Ÿคž", "๐ŸคŸ", "๐Ÿค˜", "๐Ÿค™", "๐Ÿ‘ˆ",
"๐Ÿ‘‰", "๐Ÿ‘†", "๐Ÿ–•", "๐Ÿ‘‡", "โ˜๏ธ", "๐Ÿ‘", "๐Ÿ‘Ž", "โœŠ", "๐Ÿ‘Š", "๐Ÿค›", "๐Ÿคœ", "๐Ÿ‘", "๐Ÿ™Œ", "๐Ÿ‘",
"๐Ÿคฒ", "๐Ÿค", "๐Ÿ™", "โœ๏ธ", "๐Ÿ’…", "๐Ÿคณ", "๐Ÿ’ช", "๐Ÿฆพ", "๐Ÿฆฟ", "๐Ÿฆต", "๐Ÿฆถ", "๐Ÿ‘‚", "๐Ÿฆป", "๐Ÿ‘ƒ",
"๐Ÿง ", "๐Ÿซ€", "๐Ÿซ", "๐Ÿฆท", "๐Ÿฆด", "๐Ÿ‘€", "๐Ÿ‘๏ธ", "๐Ÿ‘…", "๐Ÿ‘„", "๐Ÿ’‹",
],
),
(
"hearts",
"โค๏ธ",
&[
"โค๏ธ", "๐Ÿงก", "๐Ÿ’›", "๐Ÿ’š", "๐Ÿ’™", "๐Ÿ’œ", "๐Ÿ–ค", "๐Ÿค", "๐ŸคŽ", "๐Ÿ’”", "โฃ๏ธ", "๐Ÿ’•", "๐Ÿ’ž", "๐Ÿ’“",
"๐Ÿ’—", "๐Ÿ’–", "๐Ÿ’˜", "๐Ÿ’", "๐Ÿ’Ÿ", "โ™ฅ๏ธ", "๐Ÿ’Œ", "๐Ÿ’", "๐ŸŒน", "๐Ÿฅ€", "๐ŸŒท",
],
),
(
"celebration",
"๐ŸŽ‰",
&[
"๐ŸŽ‰", "๐ŸŽŠ", "๐ŸŽˆ", "๐ŸŽ", "๐ŸŽ€", "๐Ÿช…", "๐Ÿช†", "๐ŸŽ‚", "๐Ÿฐ", "๐Ÿง", "๐Ÿฅณ", "๐Ÿฅ‚", "๐Ÿพ", "โœจ",
"๐ŸŽ†", "๐ŸŽ‡", "๐ŸŽ„", "๐ŸŽƒ", "๐ŸŽ—๏ธ", "๐Ÿ†", "๐Ÿฅ‡", "๐Ÿฅˆ", "๐Ÿฅ‰", "๐Ÿ…", "๐ŸŽ–๏ธ", "๐ŸŽญ", "๐ŸŽช",
],
),
(
"animals",
"๐Ÿถ",
&[
"๐Ÿถ", "๐Ÿฑ", "๐Ÿญ", "๐Ÿน", "๐Ÿฐ", "๐ŸฆŠ", "๐Ÿป", "๐Ÿผ", "๐Ÿปโ€โ„๏ธ", "๐Ÿจ", "๐Ÿฏ", "๐Ÿฆ", "๐Ÿฎ",
"๐Ÿท", "๐Ÿธ", "๐Ÿต", "๐Ÿ™ˆ", "๐Ÿ™‰", "๐Ÿ™Š", "๐Ÿ”", "๐Ÿง", "๐Ÿฆ", "๐Ÿค", "๐Ÿฆ†", "๐Ÿฆ…", "๐Ÿฆ‰", "๐Ÿฆ‡",
"๐Ÿบ", "๐Ÿ—", "๐Ÿด", "๐Ÿฆ„", "๐Ÿ", "๐Ÿชฑ", "๐Ÿ›", "๐Ÿฆ‹", "๐ŸŒ", "๐Ÿž", "๐Ÿœ", "๐Ÿชฐ", "๐Ÿชฒ", "๐Ÿชณ",
"๐ŸฆŸ", "๐Ÿฆ—", "๐Ÿ•ท๏ธ", "๐Ÿฆ‚", "๐Ÿข", "๐Ÿ", "๐ŸฆŽ", "๐Ÿฆ–", "๐Ÿฆ•", "๐Ÿ™", "๐Ÿฆ‘", "๐Ÿฆ", "๐Ÿฆž", "๐Ÿฆ€",
"๐Ÿก", "๐Ÿ ", "๐ŸŸ", "๐Ÿฌ", "๐Ÿณ", "๐Ÿ‹", "๐Ÿฆˆ", "๐ŸŠ",
],
),
(
"food",
"๐Ÿ•",
&[
"๐Ÿ•", "๐Ÿ”", "๐ŸŸ", "๐ŸŒญ", "๐Ÿฟ", "๐Ÿง‚", "๐Ÿฅ“", "๐Ÿฅš", "๐Ÿณ", "๐Ÿง‡", "๐Ÿฅž", "๐Ÿงˆ", "๐Ÿž", "๐Ÿฅ",
"๐Ÿฅ–", "๐Ÿฅจ", "๐Ÿง€", "๐Ÿฅ—", "๐Ÿฅ™", "๐Ÿฅช", "๐ŸŒฎ", "๐ŸŒฏ", "๐Ÿซ”", "๐Ÿฅซ", "๐Ÿ", "๐Ÿœ", "๐Ÿฒ", "๐Ÿ›",
"๐Ÿฃ", "๐Ÿฑ", "๐ŸฅŸ", "๐Ÿฆช", "๐Ÿค", "๐Ÿ™", "๐Ÿš", "๐Ÿ˜", "๐Ÿฅ", "๐Ÿฅ ", "๐Ÿฅฎ", "๐Ÿข", "๐Ÿก", "๐Ÿง",
"๐Ÿจ", "๐Ÿฆ", "๐Ÿฅง", "๐Ÿง", "๐Ÿฐ", "๐ŸŽ‚", "๐Ÿฎ", "๐Ÿญ", "๐Ÿฌ", "๐Ÿซ", "๐Ÿฉ", "๐Ÿช", "๐ŸŒฐ", "๐Ÿฅœ",
"โ˜•", "๐Ÿต", "๐Ÿงƒ", "๐Ÿฅค", "๐Ÿง‹", "๐Ÿถ", "๐Ÿบ", "๐Ÿป", "๐Ÿฅ‚", "๐Ÿท", "๐Ÿฅƒ", "๐Ÿธ", "๐Ÿน", "๐Ÿง‰",
"๐Ÿพ",
],
),
(
"objects",
"๐Ÿ’ก",
&[
"๐Ÿ’ก", "๐Ÿ”ฆ", "๐Ÿฎ", "๐Ÿ“ฑ", "๐Ÿ’ป", "๐Ÿ–ฅ๏ธ", "๐Ÿ–จ๏ธ", "โŒจ๏ธ", "๐Ÿ–ฑ๏ธ", "๐Ÿ–ฒ๏ธ", "๐Ÿ’ฝ", "๐Ÿ’พ", "๐Ÿ’ฟ",
"๐Ÿ“€", "๐ŸŽฅ", "๐Ÿ“ท", "๐Ÿ“น", "๐Ÿ“ผ", "๐Ÿ”", "๐Ÿ”Ž", "๐Ÿ”ฌ", "๐Ÿ”ญ", "๐Ÿ“ก", "๐Ÿ’‰", "๐Ÿฉธ", "๐Ÿ’Š", "๐Ÿฉน",
"๐Ÿฉบ", "๐Ÿšช", "๐Ÿ›—", "๐Ÿชž", "๐ŸชŸ", "๐Ÿ›๏ธ", "๐Ÿ›‹๏ธ", "๐Ÿช‘", "๐Ÿšฝ", "๐Ÿช ", "๐Ÿšฟ", "๐Ÿ›", "๐Ÿชค", "๐Ÿช’",
"๐Ÿงด", "๐Ÿงท", "๐Ÿงน", "๐Ÿงบ", "๐Ÿงป", "๐Ÿชฃ", "๐Ÿงผ", "๐Ÿชฅ", "๐Ÿงฝ", "๐Ÿงฏ", "๐Ÿ›’", "๐Ÿšฌ", "โšฐ๏ธ", "๐Ÿชฆ",
"โšฑ๏ธ", "๐Ÿ—ฟ", "๐Ÿชง", "๐Ÿง",
],
),
(
"symbols",
"โœ…",
&[
"โœ…", "โŒ", "โ“", "โ—", "โ€ผ๏ธ", "โ‰๏ธ", "๐Ÿ’ฏ", "๐Ÿ”ด", "๐ŸŸ ", "๐ŸŸก", "๐ŸŸข", "๐Ÿ”ต", "๐ŸŸฃ", "โšซ",
"โšช", "๐ŸŸค", "๐Ÿ”ถ", "๐Ÿ”ท", "๐Ÿ”ธ", "๐Ÿ”น", "๐Ÿ”บ", "๐Ÿ”ป", "๐Ÿ’ ", "๐Ÿ”˜", "๐Ÿ”ณ", "๐Ÿ”ฒ", "๐Ÿ", "๐Ÿšฉ",
"๐ŸŽŒ", "๐Ÿด", "๐Ÿณ๏ธ", "โฌ›", "โฌœ", "โ—ผ๏ธ", "โ—ป๏ธ", "โ—พ", "โ—ฝ", "โ–ช๏ธ", "โ–ซ๏ธ", "๐Ÿ”ˆ", "๐Ÿ”‡", "๐Ÿ”‰",
"๐Ÿ”Š", "๐Ÿ””", "๐Ÿ”•", "๐Ÿ“ฃ", "๐Ÿ“ข", "๐Ÿ’ฌ", "๐Ÿ’ญ", "๐Ÿ—ฏ๏ธ", "โ™ ๏ธ", "โ™ฃ๏ธ", "โ™ฅ๏ธ", "โ™ฆ๏ธ", "๐Ÿƒ",
"๐ŸŽด", "๐Ÿ€„",
],
),
];

/// Common/frequently used emojis (shown first) - 4x4 grid
pub const FREQUENT_EMOJIS: &[&str] = &[
"๐Ÿ‘", "โค๏ธ", "๐Ÿ˜‚", "๐Ÿ‘€",
"๐Ÿ”ฅ", "๐Ÿ˜ฎ", "๐Ÿ˜ข", "๐Ÿ˜ก",
"๐ŸŽ‰", "๐Ÿค”", "๐Ÿ‘‹", "โœ…",
"๐Ÿ’ฏ", "๐Ÿ™", "๐Ÿ˜", "๐Ÿ‘",
];

#[derive(Props, Clone, PartialEq)]
pub struct EmojiPickerProps {
/// Called when an emoji is selected
pub on_select: EventHandler<String>,
/// Called when the picker should close
pub on_close: EventHandler<()>,
}

/// Simple emoji picker with a 4x4 grid of popular emojis
#[component]
pub fn EmojiPicker(props: EmojiPickerProps) -> Element {
rsx! {
div {
class: "bg-panel rounded-lg shadow-lg border border-border p-1.5",
// Prevent clicks inside from bubbling
onclick: move |e| e.stop_propagation(),

// 4x4 emoji grid with tight spacing
div {
class: "grid",
style: "grid-template-columns: repeat(4, 1fr); gap: 2px;",
for emoji in FREQUENT_EMOJIS.iter() {
{render_emoji_button(emoji, &props.on_select, &props.on_close)}
}
}
}
}
}

fn render_emoji_button(
emoji: &str,
on_select: &EventHandler<String>,
on_close: &EventHandler<()>,
) -> Element {
let emoji_str = emoji.to_string();
let on_select = on_select.clone();
let on_close = on_close.clone();

rsx! {
button {
key: "{emoji}",
class: "p-1 rounded hover:bg-surface transition-colors text-xl leading-none",
onclick: move |_| {
on_select.call(emoji_str.clone());
on_close.call(());
},
"{emoji}"
}
}
}
7 changes: 3 additions & 4 deletions ui/src/components/conversation/message_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ use dioxus::prelude::*;
use river_core::room_state::member::MemberId;
use river_core::room_state::message::MessageId;

/// Common emoji reactions to show in the picker
pub const REACTION_EMOJIS: &[&str] = &["๐Ÿ‘", "โค๏ธ", "๐Ÿ˜‚", "๐Ÿ˜ฎ", "๐Ÿ˜ข", "๐Ÿ˜ก", "๐ŸŽ‰", "๐Ÿค”", "๐Ÿ‘‹"];
use super::emoji_picker::FREQUENT_EMOJIS;

/// Props for the message actions component
#[derive(Props, Clone, PartialEq)]
Expand Down Expand Up @@ -48,7 +47,7 @@ pub fn MessageActions(props: MessageActionsProps) -> Element {
div {
class: "absolute top-full left-0 mt-1 bg-panel rounded-lg shadow-lg border border-border p-2 z-50",
div { class: "flex flex-wrap gap-1 max-w-[200px]",
{REACTION_EMOJIS.iter().map(|emoji| {
{FREQUENT_EMOJIS.iter().map(|emoji| {
let emoji_str = emoji.to_string();
let msg_id = message_id.clone();
rsx! {
Expand Down Expand Up @@ -116,7 +115,7 @@ pub fn QuickReactionButton(props: QuickReactionProps) -> Element {
if show_picker() {
div {
class: "absolute top-full right-0 mt-1 bg-panel rounded-lg shadow-lg border border-border p-1 flex gap-0.5 z-50",
{REACTION_EMOJIS.iter().take(6).map(|emoji| {
{FREQUENT_EMOJIS.iter().take(6).map(|emoji| {
let emoji_str = emoji.to_string();
let message_id = props.message_id.clone();
rsx! {
Expand Down
45 changes: 41 additions & 4 deletions ui/src/components/conversation/message_input.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
use dioxus::logger::tracing::*;
use dioxus::prelude::*;

use super::emoji_picker::EmojiPicker;

/// Message input component that owns its own state.
/// This isolates keystroke handling from the parent component,
/// preventing expensive re-renders of the message list on each keystroke.
#[component]
pub fn MessageInput(handle_send_message: EventHandler<String>) -> Element {
// Own the message state locally - keystrokes only re-render this component
let mut message_text = use_signal(|| String::new());
let mut show_emoji_picker = use_signal(|| false);

let mut send_message = move || {
let text = message_text.peek().to_string();
Expand All @@ -17,10 +19,46 @@ pub fn MessageInput(handle_send_message: EventHandler<String>) -> Element {
}
};

// Handle emoji selection - insert at end of message
let handle_emoji_select = move |emoji: String| {
let current = message_text.peek().to_string();
message_text.set(format!("{}{}", current, emoji));
};

rsx! {
div { class: "flex-shrink-0 border-t border-border bg-panel",
// Backdrop for emoji picker - outside the message bar to avoid z-index issues
if show_emoji_picker() {
div {
class: "fixed inset-0 z-40",
onclick: move |_| show_emoji_picker.set(false),
}
}
div { class: "flex-shrink-0 border-t border-border bg-panel relative z-50",
div { class: "max-w-4xl mx-auto px-4 py-3",
div { class: "flex gap-3 items-end",
div { class: "flex gap-3 items-center",
// Emoji picker button and popup
div { class: "relative self-center",
button {
class: "p-2.5 rounded-xl hover:bg-surface transition-colors",
title: "Insert emoji",
onclick: move |_| show_emoji_picker.set(!show_emoji_picker()),
span {
class: "text-lg",
style: "filter: grayscale(100%); opacity: 0.6;",
"๐Ÿ™‚"
}
}
// Emoji picker popup (appears above the button)
if show_emoji_picker() {
div {
class: "absolute bottom-full left-0 mb-2",
EmojiPicker {
on_select: handle_emoji_select,
on_close: move |_| show_emoji_picker.set(false),
}
}
}
}
textarea {
class: "flex-1 px-4 py-2.5 bg-surface border border-border rounded-xl text-text placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-accent/50 focus:border-accent transition-colors resize-none min-h-[44px] max-h-[200px]",
placeholder: "Type your message...",
Expand All @@ -39,7 +77,6 @@ pub fn MessageInput(handle_send_message: EventHandler<String>) -> Element {
button {
class: "px-5 py-2.5 bg-accent hover:bg-accent-hover text-white font-medium rounded-xl transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
onclick: move |_| {
info!("Send button clicked");
send_message();
},
"Send"
Expand Down