mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Implement emoji panel
This commit is contained in:
@@ -536,6 +536,18 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.emoji-btn {
|
||||||
|
margin-left: 10px;
|
||||||
|
color: $color-dark-on-surface-variant;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $color-dark-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.message-input {
|
.message-input {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -763,3 +775,130 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emoji Menu Styles
|
||||||
|
.emoji-menu {
|
||||||
|
background: $color-dark-surface-container;
|
||||||
|
border: 1px solid rgba($color-dark-outline-variant, 0.4);
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
width: 320px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transform-origin: bottom left;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-menu-header {
|
||||||
|
background: $color-dark-surface-container-high;
|
||||||
|
border-bottom: 1px solid rgba($color-dark-outline-variant, 0.2);
|
||||||
|
padding: 8px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.emoji-category-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: $color-dark-surface-container;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-category-tab {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
min-width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $color-dark-surface-container;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: $color-dark-primary-container;
|
||||||
|
color: $color-dark-on-primary-container;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(8, 1fr);
|
||||||
|
gap: 2px;
|
||||||
|
padding: 8px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: $color-dark-surface-container-high;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-item {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 40px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $color-dark-surface-container-high;
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-empty-state {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: $color-dark-on-surface-variant;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { MaterialDialog } from "../core/Dialog";
|
import { MaterialDialog } from "../core/Dialog";
|
||||||
import { RichTextArea } from "../core/RichTextArea";
|
import { RichTextArea } from "../core/RichTextArea";
|
||||||
import type { Message } from "../../../core/types";
|
import type { Message } from "../../../core/types";
|
||||||
import Quote from "../core/Quote";
|
import Quote from "../core/Quote";
|
||||||
import AnimatedHeight from "../core/animations/AnimatedHeight";
|
import AnimatedHeight from "../core/animations/AnimatedHeight";
|
||||||
import { useImmer } from "use-immer";
|
import { useImmer } from "use-immer";
|
||||||
|
import { EmojiMenu } from "./EmojiMenu";
|
||||||
|
|
||||||
interface ChatInputWrapperProps {
|
interface ChatInputWrapperProps {
|
||||||
onSendMessage: (message: string, files: File[]) => void;
|
onSendMessage: (message: string, files: File[]) => void;
|
||||||
@@ -18,6 +19,7 @@ interface ChatInputWrapperProps {
|
|||||||
onClearEdit?: () => void;
|
onClearEdit?: () => void;
|
||||||
onCloseEdit?: () => void;
|
onCloseEdit?: () => void;
|
||||||
onProvideFileAdder?: (adder: (files: File[]) => void) => void;
|
onProvideFileAdder?: (adder: (files: File[]) => void) => void;
|
||||||
|
messagePanelRef?: React.RefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatInputWrapper(
|
export function ChatInputWrapper(
|
||||||
@@ -32,13 +34,18 @@ export function ChatInputWrapper(
|
|||||||
editVisible = false,
|
editVisible = false,
|
||||||
onClearEdit,
|
onClearEdit,
|
||||||
onCloseEdit,
|
onCloseEdit,
|
||||||
onProvideFileAdder
|
onProvideFileAdder,
|
||||||
|
messagePanelRef
|
||||||
}: ChatInputWrapperProps
|
}: ChatInputWrapperProps
|
||||||
) {
|
) {
|
||||||
const [message, setMessage] = useState("");
|
const [message, setMessage] = useState("");
|
||||||
const [selectedFiles, setSelectedFiles] = useImmer<File[]>([]);
|
const [selectedFiles, setSelectedFiles] = useImmer<File[]>([]);
|
||||||
const [attachmentsVisible, setAttachmentsVisible] = useState(false);
|
const [attachmentsVisible, setAttachmentsVisible] = useState(false);
|
||||||
const [errorOpen, setErrorOpen] = useState(false);
|
const [errorOpen, setErrorOpen] = useState(false);
|
||||||
|
const [emojiMenuOpen, setEmojiMenuOpen] = useState(false);
|
||||||
|
const [emojiMenuPosition, setEmojiMenuPosition] = useState({ x: 0, y: 0 });
|
||||||
|
const emojiButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const chatInputWrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Expose a way for parent to programmatically add files
|
// Expose a way for parent to programmatically add files
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -60,7 +67,26 @@ export function ChatInputWrapper(
|
|||||||
setAttachmentsVisible(selectedFiles.length > 0);
|
setAttachmentsVisible(selectedFiles.length > 0);
|
||||||
}, [selectedFiles]);
|
}, [selectedFiles]);
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent | Event) => {
|
function handleEmojiButtonClick() {
|
||||||
|
if (chatInputWrapperRef.current && messagePanelRef?.current) {
|
||||||
|
const inputRect = chatInputWrapperRef.current.getBoundingClientRect();
|
||||||
|
const panelRect = messagePanelRef.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Position menu 10px from message panel edge and 10px above the chat input
|
||||||
|
// The animation will start 30px below this position
|
||||||
|
setEmojiMenuPosition({
|
||||||
|
x: panelRect.left + 10, // 10px from message panel edge
|
||||||
|
y: window.innerHeight - inputRect.top + 10 // 10px above the top of chat input
|
||||||
|
});
|
||||||
|
setEmojiMenuOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleEmojiSelect(emoji: string) {
|
||||||
|
setMessage(prev => prev + emoji);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function handleSubmit(e: React.FormEvent | Event) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const hasText = Boolean(message.trim());
|
const hasText = Boolean(message.trim());
|
||||||
const hasFiles = selectedFiles.length > 0;
|
const hasFiles = selectedFiles.length > 0;
|
||||||
@@ -95,7 +121,7 @@ export function ChatInputWrapper(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-input-wrapper">
|
<div className="chat-input-wrapper" ref={chatInputWrapperRef}>
|
||||||
<form className="input-group" id="message-form" onSubmit={handleSubmit}>
|
<form className="input-group" id="message-form" onSubmit={handleSubmit}>
|
||||||
<AnimatedHeight visible={editVisible} onFinish={onCloseEdit}>
|
<AnimatedHeight visible={editVisible} onFinish={onCloseEdit}>
|
||||||
{editingMessage && (
|
{editingMessage && (
|
||||||
@@ -150,6 +176,12 @@ export function ChatInputWrapper(
|
|||||||
)}
|
)}
|
||||||
</AnimatedHeight>
|
</AnimatedHeight>
|
||||||
<div className="chat-input">
|
<div className="chat-input">
|
||||||
|
<mdui-button-icon
|
||||||
|
ref={emojiButtonRef}
|
||||||
|
icon="mood"
|
||||||
|
onClick={handleEmojiButtonClick}
|
||||||
|
className="emoji-btn"
|
||||||
|
></mdui-button-icon>
|
||||||
<RichTextArea
|
<RichTextArea
|
||||||
className="message-input"
|
className="message-input"
|
||||||
id="message-input"
|
id="message-input"
|
||||||
@@ -172,6 +204,13 @@ export function ChatInputWrapper(
|
|||||||
<div>Общий размер вложений превышает 4 ГБ.</div>
|
<div>Общий размер вложений превышает 4 ГБ.</div>
|
||||||
<mdui-button slot="action" onClick={() => setErrorOpen(false)}>Закрыть</mdui-button>
|
<mdui-button slot="action" onClick={() => setErrorOpen(false)}>Закрыть</mdui-button>
|
||||||
</MaterialDialog>
|
</MaterialDialog>
|
||||||
|
|
||||||
|
<EmojiMenu
|
||||||
|
isOpen={emojiMenuOpen}
|
||||||
|
onClose={() => setEmojiMenuOpen(false)}
|
||||||
|
onEmojiSelect={handleEmojiSelect}
|
||||||
|
position={emojiMenuPosition}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { EMOJI_CATEGORIES, getRecentEmojis, addRecentEmoji } from "./emojiData";
|
||||||
|
import type { Size2D } from "../../../core/types";
|
||||||
|
|
||||||
|
interface EmojiMenuProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onEmojiSelect: (emoji: string) => void;
|
||||||
|
position: Size2D;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EmojiMenu({ isOpen, onClose, onEmojiSelect, position }: EmojiMenuProps) {
|
||||||
|
const [activeCategory, setActiveCategory] = useState("recent");
|
||||||
|
const [recentEmojis, setRecentEmojis] = useState<string[]>([]);
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setRecentEmojis(getRecentEmojis());
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEscape(event: KeyboardEvent) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
document.addEventListener("keydown", handleEscape);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
document.removeEventListener("keydown", handleEscape);
|
||||||
|
};
|
||||||
|
}, [isOpen, onClose]);
|
||||||
|
|
||||||
|
function handleEmojiClick(emoji: string) {
|
||||||
|
addRecentEmoji(emoji);
|
||||||
|
onEmojiSelect(emoji);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
function getCurrentEmojis(): string[] {
|
||||||
|
if (activeCategory === "recent") {
|
||||||
|
return recentEmojis;
|
||||||
|
}
|
||||||
|
const category = EMOJI_CATEGORIES.find(cat => cat.name === activeCategory);
|
||||||
|
return category?.emojis || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={menuRef}
|
||||||
|
className={`emoji-menu ${isOpen ? "open" : ""}`}
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
left: position.x,
|
||||||
|
bottom: position.y,
|
||||||
|
zIndex: 1000,
|
||||||
|
pointerEvents: isOpen ? "auto" : "none"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="emoji-menu-header">
|
||||||
|
<div className="emoji-category-tabs">
|
||||||
|
{EMOJI_CATEGORIES.map((category) => (
|
||||||
|
<button
|
||||||
|
key={category.name}
|
||||||
|
className={`emoji-category-tab ${activeCategory === category.name ? "active" : ""}`}
|
||||||
|
onClick={() => setActiveCategory(category.name)}
|
||||||
|
title={category.name}
|
||||||
|
>
|
||||||
|
{category.icon}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="emoji-grid">
|
||||||
|
{getCurrentEmojis().map((emoji, index) => (
|
||||||
|
<button
|
||||||
|
key={`${activeCategory}-${index}`}
|
||||||
|
className="emoji-item"
|
||||||
|
onClick={() => handleEmojiClick(emoji)}
|
||||||
|
title={emoji}
|
||||||
|
>
|
||||||
|
{emoji}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{getCurrentEmojis().length === 0 && activeCategory === "recent" && (
|
||||||
|
<div className="emoji-empty-state">
|
||||||
|
<span>No recent emojis</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ interface MessagePanelRendererProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRendererProps) {
|
export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRendererProps) {
|
||||||
|
const messagePanelRef = useRef<HTMLDivElement>(null);
|
||||||
const [panelState, setPanelState] = useState<MessagePanelState | null>(null);
|
const [panelState, setPanelState] = useState<MessagePanelState | null>(null);
|
||||||
const [switchIn, setSwitchIn] = useState(false);
|
const [switchIn, setSwitchIn] = useState(false);
|
||||||
const [switchOut, setSwitchOut] = useState(false);
|
const [switchOut, setSwitchOut] = useState(false);
|
||||||
@@ -145,6 +146,7 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
|
|||||||
return (
|
return (
|
||||||
<div className={`chat-container ${switchIn ? "chat-switch-in" : ""} ${switchOut ? "chat-switch-out" : ""}`}>
|
<div className={`chat-container ${switchIn ? "chat-switch-in" : ""} ${switchOut ? "chat-switch-out" : ""}`}>
|
||||||
<div
|
<div
|
||||||
|
ref={messagePanelRef}
|
||||||
className="chat-main"
|
className="chat-main"
|
||||||
id="chat-inner"
|
id="chat-inner"
|
||||||
onDragEnter={(e) => {
|
onDragEnter={(e) => {
|
||||||
@@ -290,6 +292,7 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onProvideFileAdder={(adder) => { addFilesRef.current = adder; }}
|
onProvideFileAdder={(adder) => { addFilesRef.current = adder; }}
|
||||||
|
messagePanelRef={messagePanelRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
export interface EmojiCategory {
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
emojis: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EMOJI_CATEGORIES: EmojiCategory[] = [
|
||||||
|
{
|
||||||
|
name: "recent",
|
||||||
|
icon: "🕒",
|
||||||
|
emojis: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "smileys",
|
||||||
|
icon: "😀",
|
||||||
|
emojis: [
|
||||||
|
"😀", "😃", "😄", "😁", "😆", "😅", "🤣", "😂", "🙂", "🙃", "😉", "😊", "😇", "🥰", "😍", "🤩", "😘", "😗", "😚", "😙", "😋", "😛", "😜", "🤪", "😝", "🤑", "🤗", "🤭", "🤫", "🤔", "🤐", "🤨", "😐", "😑", "😶", "😏", "😒", "🙄", "😬", "🤥", "😔", "😪", "🤤", "😴", "😷", "🤒", "🤕", "🤢", "🤮", "🤧", "🥵", "🥶", "🥴", "😵", "🤯", "🤠", "🥳", "😎", "🤓", "🧐", "😕", "😟", "🙁", "☹️", "😮", "😯", "😲", "😳", "🥺", "😦", "😧", "😨", "😰", "😥", "😢", "😭", "😱", "😖", "😣", "😞", "😓", "😩", "😫", "🥱", "😤", "😡", "😠", "🤬", "😈", "👿", "💀", "☠️", "💩", "🤡", "👹", "👺", "👻", "👽", "👾", "🤖", "😺", "😸", "😹", "😻", "😼", "😽", "🙀", "😿", "😾"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "people",
|
||||||
|
icon: "👋",
|
||||||
|
emojis: [
|
||||||
|
"👋", "🤚", "🖐", "✋", "🖖", "👌", "🤏", "✌️", "🤞", "🤟", "🤘", "🤙", "👈", "👉", "👆", "🖕", "👇", "☝️", "👍", "👎", "👊", "✊", "🤛", "🤜", "👏", "🙌", "👐", "🤲", "🤝", "🙏", "✍️", "💅", "🤳", "💪", "🦾", "🦿", "🦵", "🦶", "👂", "🦻", "👃", "🧠", "🦷", "🦴", "👀", "👁", "👅", "👄", "💋", "🩸", "👶", "🧒", "👦", "👧", "🧑", "👨", "👩", "🧓", "👴", "👵", "👱", "🧔", "👲", "🧕", "👳", "👮", "👷", "💂", "🕵️", "👩⚕️", "👨⚕️", "👩🌾", "👨🌾", "👩🍳", "👨🍳", "👩🎓", "👨🎓", "👩🎤", "👨🎤", "👩🏫", "👨🏫", "👩🏭", "👨🏭", "👩💻", "👨💻", "👩💼", "👨💼", "👩🔧", "👨🔧", "👩🔬", "👨🔬", "👩🎨", "👨🎨", "👩🚒", "👨🚒", "👩✈️", "👨✈️", "👩🚀", "👨🚀", "👩⚖️", "👨⚖️", "👰", "🤵", "👸", "🤴", "🦸", "🦹", "🤶", "🎅", "🧙", "🧚", "🧛", "🧜", "🧝", "🧞", "🧟", "💆", "💇", "🚶", "🏃", "💃", "🕺", "🕴", "👯", "🧘", "🛀", "🛌", "👭", "👫", "👬", "💏", "💑", "👪"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "animals",
|
||||||
|
icon: "🐶",
|
||||||
|
emojis: [
|
||||||
|
"🐶", "🐱", "🐭", "🐹", "🐰", "🦊", "🐻", "🐼", "🐨", "🐯", "🦁", "🐮", "🐷", "🐽", "🐸", "🐵", "🙈", "🙉", "🙊", "🐒", "🐔", "🐧", "🐦", "🐤", "🐣", "🐥", "🦆", "🦅", "🦉", "🦇", "🐺", "🐗", "🐴", "🦄", "🐝", "🐛", "🦋", "🐌", "🐞", "🐜", "🦟", "🦗", "🕷", "🕸", "🦂", "🐢", "🐍", "🦎", "🦖", "🦕", "🐙", "🦑", "🦐", "🦞", "🦀", "🐡", "🐠", "🐟", "🐬", "🐳", "🐋", "🦈", "🐊", "🐅", "🐆", "🦓", "🦍", "🦧", "🐘", "🦛", "🦏", "🐪", "🐫", "🦒", "🦘", "🐃", "🐂", "🐄", "🐎", "🐖", "🐏", "🐑", "🦙", "🐐", "🦌", "🐕", "🐩", "🦮", "🐕🦺", "🐈", "🐓", "🦃", "🦚", "🦜", "🦢", "🦩", "🕊", "🐇", "🦝", "🦨", "🦡", "🦦", "🦥", "🐁", "🐀", "🐿", "🦔"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "food",
|
||||||
|
icon: "🍎",
|
||||||
|
emojis: [
|
||||||
|
"🍎", "🍐", "🍊", "🍋", "🍌", "🍉", "🍇", "🍓", "🫐", "🍈", "🍒", "🍑", "🥭", "🍍", "🥥", "🥝", "🍅", "🍆", "🥑", "🥦", "🥬", "🥒", "🌶", "🫑", "🌽", "🥕", "🫒", "🧄", "🧅", "🥔", "🍠", "🥐", "🥯", "🍞", "🥖", "🥨", "🧀", "🥚", "🍳", "🧈", "🥞", "🧇", "🥓", "🥩", "🍗", "🍖", "🦴", "🌭", "🍔", "🍟", "🍕", "🫓", "🥙", "🌮", "🌯", "🫔", "🥗", "🥘", "🫕", "🥫", "🍝", "🍜", "🍲", "🍛", "🍣", "🍱", "🥟", "🦪", "🍤", "🍙", "🍚", "🍘", "🍥", "🥠", "🥮", "🍢", "🍡", "🍧", "🍨", "🍦", "🥧", "🧁", "🍰", "🎂", "🍮", "🍭", "🍬", "🍫", "🍿", "🍩", "🍪", "🌰", "🥜", "🍯", "🥛", "🍼", "☕", "🫖", "🍵", "🧃", "🥤", "🧋", "🍶", "🍺", "🍻", "🥂", "🍷", "🥃", "🍸", "🍹", "🧉", "🍾"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "travel",
|
||||||
|
icon: "🚗",
|
||||||
|
emojis: [
|
||||||
|
"🚗", "🚕", "🚙", "🚌", "🚎", "🏎", "🚓", "🚑", "🚒", "🚐", "🛻", "🚚", "🚛", "🚜", "🏍", "🛵", "🚲", "🛴", "🛹", "🛼", "🚁", "✈️", "🛩", "🛫", "🛬", "🪂", "💺", "🚀", "🛸", "🚉", "🚊", "🚝", "🚞", "🚋", "🚃", "🚋", "🚋", "🚄", "🚅", "🚈", "🚂", "🚆", "🚇", "🚊", "🚍", "🚘", "🚖", "🚡", "🚠", "🚟", "🎢", "🎡", "🎠", "⛵", "🛥", "🚤", "⛴", "🛳", "🚢", "⚓", "🚧", "⛽", "🚨", "🚥", "🚦", "🛑", "🚏", "🗺", "🗿", "🗽", "🗼", "🏰", "🏯", "🏟", "🎡", "🎢", "🎠", "⛲", "⛱", "🏖", "🏝", "🏔", "⛰", "🌋", "🗻", "🏕", "⛺", "🏠", "🏡", "🏘", "🏚", "🏗", "🏭", "🏢", "🏬", "🏣", "🏤", "🏥", "🏦", "🏨", "🏪", "🏫", "🏩", "💒", "🏛", "⛪", "🕌", "🛕", "🕍", "🕋", "⛩", "🛤", "🛣", "🗾", "🎑", "🏞", "🌅", "🌄", "🌠", "🎇", "🎆", "🌇", "🌆", "🏙", "🌃", "🌌", "🌉", "🌁"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "activities",
|
||||||
|
icon: "⚽",
|
||||||
|
emojis: [
|
||||||
|
"⚽", "🏀", "🏈", "⚾", "🥎", "🎾", "🏐", "🏉", "🎱", "🪀", "🏓", "🏸", "🏒", "🏑", "🥍", "🏏", "🪃", "🥅", "⛳", "🪁", "🏹", "🎣", "🤿", "🥊", "🥋", "🎽", "🛹", "🛷", "⛸", "🥌", "🎿", "⛷", "🏂", "🪂", "🏋️♀️", "🏋️♂️", "🤼♀️", "🤼♂️", "🤸♀️", "🤸♂️", "⛹️♀️", "⛹️♂️", "🤺", "🤾♀️", "🤾♂️", "🏌️♀️", "🏌️♂️", "🏇", "🧘♀️", "🧘♂️", "🏄♀️", "🏄♂️", "🏊♀️", "🏊♂️", "🤽♀️", "🤽♂️", "🚣♀️", "🚣♂️", "🧗♀️", "🧗♂️", "🚵♀️", "🚵♂️", "🚴♀️", "🚴♂️", "🏆", "🥇", "🥈", "🥉", "🏅", "🎖", "🏵", "🎗", "🎫", "🎟", "🎪", "🤹", "🤹♀️", "🤹♂️", "🎭", "🩰", "🎨", "🎬", "🎤", "🎧", "🎼", "🎹", "🥁", "🎷", "🎺", "🎸", "🪕", "🎻", "🎲", "♠️", "♥️", "♦️", "♣️", "♟", "🃏", "🀄", "🎴", "🎯", "🎳", "🎮", "🎰", "🧩"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "objects",
|
||||||
|
icon: "📱",
|
||||||
|
emojis: [
|
||||||
|
"📱", "📲", "☎️", "📞", "📟", "📠", "🔋", "🔌", "💻", "🖥", "🖨", "⌨️", "🖱", "🖲", "💽", "💾", "💿", "📀", "🧮", "🎥", "📽", "📸", "📹", "📷", "🔍", "🔎", "🕯", "💡", "🔦", "🏮", "🪔", "📔", "📕", "📖", "📗", "📘", "📙", "📚", "📓", "📒", "📃", "📜", "📄", "📰", "🗞", "📑", "🔖", "🏷", "💰", "💴", "💵", "💶", "💷", "💸", "💳", "🧾", "💹", "💱", "💲", "✉️", "📧", "📨", "📩", "📤", "📥", "📦", "📫", "📪", "📬", "📭", "📮", "🗳", "✏️", "✒️", "🖋", "🖊", "🖌", "🖍", "📝", "💼", "📁", "📂", "🗂", "📅", "📆", "🗒", "🗓", "📇", "📈", "📉", "📊", "📋", "📌", "📍", "📎", "🖇", "📏", "📐", "✂️", "🗃", "🗄", "🗑", "🔒", "🔓", "🔏", "🔐", "🔑", "🗝", "🔨", "⛏", "⚒", "🛠", "🗡", "⚔️", "🔫", "🪃", "🏹", "🛡", "🪚", "🔧", "🪛", "🔩", "⚙️", "🗜", "⚖️", "🦯", "🔗", "⛓", "🧰", "🧲", "⚗️", "🧪", "🧫", "🧬", "🔬", "🔭", "📡", "💉", "💊", "🩹", "🩺", "🚪", "🛏", "🛋", "🚽", "🚿", "🛁", "🛀", "🧴", "🧷", "🧹", "🧺", "🧻", "🚰", "🚰", "🪒", "🧽", "🧯", "🛒"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "symbols",
|
||||||
|
icon: "❤️",
|
||||||
|
emojis: [
|
||||||
|
"❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "🤍", "🤎", "💔", "❣️", "💕", "💞", "💓", "💗", "💖", "💘", "💝", "💟", "☮️", "✝️", "☪️", "🕉", "☸️", "✡️", "🔯", "🕎", "☯️", "☦️", "🛐", "⛎", "♈", "♉", "♊", "♋", "♌", "♍", "♎", "♏", "♐", "♑", "♒", "♓", "🆔", "⚛️", "🉑", "☢️", "☣️", "📴", "📳", "🈶", "🈚", "🈸", "🈺", "🈷️", "✴️", "🆚", "💮", "🉐", "㊙️", "㊗️", "🈴", "🈵", "🈹", "🈲", "🅰️", "🅱️", "🆎", "🅾️", "🆘", "❌", "⭕", "🛑", "⛔", "📛", "🚫", "💯", "💢", "♨️", "🚷", "🚯", "🚳", "🚱", "🔞", "📵", "🚭", "❗", "❕", "❓", "❔", "‼️", "⁉️", "🔅", "🔆", "〽️", "⚠️", "🚸", "🔱", "⚜️", "🔰", "♻️", "✅", "🈯", "💹", "❇️", "✳️", "❎", "🌐", "💠", "Ⓜ️", "🌀", "💤", "🏧", "🚾", "♿", "🅿️", "🛗", "🈳", "🈂️", "🛂", "🛃", "🛄", "🛅", "🚹", "🚺", "🚼", "⚧", "🚻", "🚮", "🎦", "📶", "🈁", "🔣", "ℹ️", "🔤", "🔡", "🔠", "🆖", "🆗", "🆙", "🆒", "🆕", "🆓", "0️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "flags",
|
||||||
|
icon: "🏳️",
|
||||||
|
emojis: [
|
||||||
|
"🏳️", "🏴", "🏁", "🚩", "🏳️🌈", "🏳️⚧️", "🏴☠️", "🇦🇨", "🇦🇩", "🇦🇪", "🇦🇫", "🇦🇬", "🇦🇮", "🇦🇱", "🇦🇲", "🇦🇴", "🇦🇶", "🇦🇷", "🇦🇸", "🇦🇹", "🇦🇺", "🇦🇼", "🇦🇽", "🇦🇿", "🇧🇦", "🇧🇧", "🇧🇩", "🇧🇪", "🇧🇫", "🇧🇬", "🇧🇭", "🇧🇮", "🇧🇯", "🇧🇱", "🇧🇲", "🇧🇳", "🇧🇴", "🇧🇶", "🇧🇷", "🇧🇸", "🇧🇹", "🇧🇻", "🇧🇼", "🇧🇾", "🇧🇿", "🇨🇦", "🇨🇨", "🇨🇩", "🇨🇫", "🇨🇬", "🇨🇭", "🇨🇮", "🇨🇰", "🇨🇱", "🇨🇲", "🇨🇳", "🇨🇴", "🇨🇵", "🇨🇷", "🇨🇺", "🇨🇻", "🇨🇼", "🇨🇽", "🇨🇾", "🇨🇿", "🇩🇪", "🇩🇬", "🇩🇯", "🇩🇰", "🇩🇲", "🇩🇴", "🇩🇿", "🇪🇦", "🇪🇨", "🇪🇪", "🇪🇬", "🇪🇭", "🇪🇷", "🇪🇸", "🇪🇹", "🇪🇺", "🇫🇮", "🇫🇯", "🇫🇰", "🇫🇲", "🇫🇴", "🇫🇷", "🇬🇦", "🇬🇧", "🇬🇩", "🇬🇪", "🇬🇫", "🇬🇬", "🇬🇭", "🇬🇮", "🇬🇱", "🇬🇲", "🇬🇳", "🇬🇵", "🇬🇶", "🇬🇷", "🇬🇸", "🇬🇹", "🇬🇺", "🇬🇼", "🇬🇾", "🇭🇰", "🇭🇲", "🇭🇳", "🇭🇷", "🇭🇹", "🇭🇺", "🇮🇨", "🇮🇩", "🇮🇪", "🇮🇱", "🇮🇲", "🇮🇳", "🇮🇴", "🇮🇶", "🇮🇷", "🇮🇸", "🇮🇹", "🇯🇪", "🇯🇲", "🇯🇴", "🇯🇵", "🇰🇪", "🇰🇬", "🇰🇭", "🇰🇮", "🇰🇲", "🇰🇳", "🇰🇵", "🇰🇷", "🇰🇼", "🇰🇾", "🇰🇿", "🇱🇦", "🇱🇧", "🇱🇨", "🇱🇮", "🇱🇰", "🇱🇷", "🇱🇸", "🇱🇹", "🇱🇺", "🇱🇻", "🇱🇾", "🇲🇦", "🇲🇨", "🇲🇩", "🇲🇪", "🇲🇫", "🇲🇬", "🇲🇭", "🇲🇰", "🇲🇱", "🇲🇲", "🇲🇳", "🇲🇴", "🇲🇵", "🇲🇶", "🇲🇷", "🇲🇸", "🇲🇹", "🇲🇺", "🇲🇻", "🇲🇼", "🇲🇽", "🇲🇾", "🇲🇿", "🇳🇦", "🇳🇨", "🇳🇪", "🇳🇫", "🇳🇬", "🇳🇮", "🇳🇱", "🇳🇴", "🇳🇵", "🇳🇷", "🇳🇺", "🇳🇿", "🇴🇲", "🇵🇦", "🇵🇪", "🇵🇫", "🇵🇬", "🇵🇭", "🇵🇰", "🇵🇱", "🇵🇲", "🇵🇳", "🇵🇷", "🇵🇸", "🇵🇹", "🇵🇼", "🇵🇾", "🇶🇦", "🇷🇪", "🇷🇴", "🇷🇸", "🇷🇺", "🇷🇼", "🇸🇦", "🇸🇧", "🇸🇨", "🇸🇩", "🇸🇪", "🇸🇬", "🇸🇭", "🇸🇮", "🇸🇯", "🇸🇰", "🇸🇱", "🇸🇲", "🇸🇳", "🇸🇴", "🇸🇷", "🇸🇸", "🇸🇹", "🇸🇻", "🇸🇽", "🇸🇾", "🇸🇿", "🇹🇦", "🇹🇨", "🇹🇩", "🇹🇫", "🇹🇬", "🇹🇭", "🇹🇯", "🇹🇰", "🇹🇱", "🇹🇲", "🇹🇳", "🇹🇴", "🇹🇷", "🇹🇹", "🇹🇻", "🇹🇼", "🇹🇿", "🇺🇦", "🇺🇬", "🇺🇲", "🇺🇸", "🇺🇾", "🇺🇿", "🇻🇦", "🇻🇨", "🇻🇪", "🇻🇬", "🇻🇮", "🇻🇳", "🇻🇺", "🇼🇫", "🇼🇸", "🇾🇪", "🇾🇹", "🇿🇦", "🇿🇲", "🇿🇼"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const RECENT_EMOJIS_KEY = "recentEmojis";
|
||||||
|
|
||||||
|
export function getRecentEmojis(): string[] {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(RECENT_EMOJIS_KEY);
|
||||||
|
return stored ? JSON.parse(stored) : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addRecentEmoji(emoji: string): void {
|
||||||
|
try {
|
||||||
|
let recentEmojis = getRecentEmojis();
|
||||||
|
recentEmojis = recentEmojis.filter(e => e !== emoji);
|
||||||
|
recentEmojis.unshift(emoji);
|
||||||
|
recentEmojis = recentEmojis.slice(0, 50);
|
||||||
|
localStorage.setItem(RECENT_EMOJIS_KEY, JSON.stringify(recentEmojis));
|
||||||
|
} catch {
|
||||||
|
// Ignore localStorage errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user