From 2ec2a4fcd3070965b27aa22bdc8892ae4cd5477f Mon Sep 17 00:00:00 2001 From: DCCONSTRUCTIONS Date: Wed, 22 Apr 2026 20:40:13 +0300 Subject: [PATCH] =?UTF-8?q?UI=20-=20=D0=9C=D0=95=D0=96=D0=9F=D0=A0=D0=9E?= =?UTF-8?q?=D0=95=D0=9A=D0=A2=D0=9D=D0=90=D0=AF=20=D0=9A=D0=9E=D0=9C=D0=9C?= =?UTF-8?q?=D0=A3=D0=9D=D0=98=D0=9A=D0=90=D0=A6=D0=98=D0=AF:=20=D0=BB?= =?UTF-8?q?=D0=BE=D0=BA=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20emoji=20popu?= =?UTF-8?q?p=20=D0=B4=D0=BB=D1=8F=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B0=D1=80=D0=B8=D0=B5=D0=B2=20=D0=B4=D0=B5=D1=82=D0=B0?= =?UTF-8?q?=D0=BB=D0=B5=D0=B9=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/editor/lite-text/toolbar.tsx | 195 ++++++++++++++++-- .../src/core/components/menus/menu-items.ts | 11 + .../src/emoji-icon-picker/emoji/emoji.tsx | 140 ++++++++++--- .../emoji-reaction/emoji-reaction-picker.tsx | 18 ++ 4 files changed, 323 insertions(+), 41 deletions(-) diff --git a/plane-src/apps/web/core/components/editor/lite-text/toolbar.tsx b/plane-src/apps/web/core/components/editor/lite-text/toolbar.tsx index c66f142..42d450e 100644 --- a/plane-src/apps/web/core/components/editor/lite-text/toolbar.tsx +++ b/plane-src/apps/web/core/components/editor/lite-text/toolbar.tsx @@ -4,7 +4,7 @@ * See the LICENSE file for details. */ -import React, { useEffect, useState, useCallback } from "react"; +import React, { useEffect, useState, useCallback, useMemo } from "react"; import { ArrowUp, Paperclip, SmilePlus } from "lucide-react"; import type { LucideIcon } from "lucide-react"; @@ -17,6 +17,7 @@ import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/propel/button"; import { GlobeIcon, LockIcon } from "@plane/propel/icons"; import type { ISvgIcons } from "@plane/propel/icons"; +import { Popover } from "@plane/propel/popover"; import { Tooltip } from "@plane/propel/tooltip"; // constants import { cn } from "@plane/utils"; @@ -58,12 +59,177 @@ const COMMENT_ACCESS_SPECIFIERS: TCommentAccessType[] = [ ]; const toolbarItems = TOOLBAR_ITEMS.lite; -const EMOJI_ITEM: ToolbarMenuItem<"emoji"> = { - itemKey: "emoji", - renderKey: "emoji", - name: "Emoji", - icon: SmilePlus, - editors: ["lite", "document"], +const COMMENT_RECENT_EMOJI_STORAGE_KEY = "nodedc-comment-emoji-recent"; +const COMMENT_RECENT_EMOJI_LIMIT = 8; +const COMMENT_EMOJIS = [ + "๐Ÿ˜€", + "๐Ÿ˜ƒ", + "๐Ÿ˜„", + "๐Ÿ˜", + "๐Ÿ˜†", + "๐Ÿ˜…", + "๐Ÿคฃ", + "๐Ÿ˜‚", + "๐Ÿ™‚", + "๐Ÿ˜‰", + "๐Ÿ˜Š", + "๐Ÿ˜‡", + "๐Ÿฅฐ", + "๐Ÿ˜", + "๐Ÿคฉ", + "๐Ÿ˜˜", + "๐Ÿ˜—", + "๐Ÿ˜š", + "๐Ÿ˜‹", + "๐Ÿ˜›", + "๐Ÿ˜œ", + "๐Ÿคช", + "๐Ÿซ ", + "๐Ÿค—", + "๐Ÿค”", + "๐Ÿซก", + "๐Ÿค", + "๐Ÿ‘", + "๐Ÿ™Œ", + "๐Ÿ‘", + "๐Ÿ‘Ž", + "๐Ÿ”ฅ", + "๐Ÿ’ฏ", + "โœจ", + "๐ŸŽ‰", + "โค๏ธ", + "๐Ÿงก", + "๐Ÿ’›", + "๐Ÿ’š", + "๐Ÿ’™", + "๐Ÿ’œ", + "๐Ÿค", + "๐ŸคŽ", + "๐Ÿ–ค", + "๐Ÿ˜Ž", + "๐Ÿค“", + "๐Ÿฅณ", + "๐Ÿ˜ด", + "๐Ÿคฏ", + "๐Ÿ˜ฌ", + "๐Ÿ˜Œ", + "๐Ÿฅฒ", + "๐Ÿ˜ญ", + "๐Ÿ˜ก", + "๐Ÿคฎ", + "๐Ÿคข", + "๐Ÿคก", + "๐Ÿ‘€", + "๐Ÿ™", + "๐Ÿ‘Œ", + "โœ…", + "โŒ", +]; + +const readRecentCommentEmojis = (storageKey: string) => { + if (typeof window === "undefined") return []; + + try { + const value = window.localStorage.getItem(storageKey); + if (!value) return []; + + const parsed = JSON.parse(value); + return Array.isArray(parsed) ? parsed.filter((emoji): emoji is string => typeof emoji === "string") : []; + } catch { + return []; + } +}; + +const writeRecentCommentEmoji = (storageKey: string, emoji: string, limit: number) => { + if (typeof window === "undefined") return []; + + const next = [emoji, ...readRecentCommentEmojis(storageKey).filter((value) => value !== emoji)].slice(0, limit); + + try { + window.localStorage.setItem(storageKey, JSON.stringify(next)); + } catch { + return next; + } + + return next; +}; + +type CompactCommentEmojiPickerProps = { + isOpen: boolean; + onEmojiSelect: (emoji: string) => void; + onOpenChange: (value: boolean) => void; +}; + +const CompactCommentEmojiPicker: React.FC = ({ + isOpen, + onEmojiSelect, + onOpenChange, +}) => { + const [recentEmojis, setRecentEmojis] = useState(() => + readRecentCommentEmojis(COMMENT_RECENT_EMOJI_STORAGE_KEY) + ); + + const handleEmojiSelect = useCallback( + (emoji: string) => { + setRecentEmojis( + writeRecentCommentEmoji(COMMENT_RECENT_EMOJI_STORAGE_KEY, emoji, COMMENT_RECENT_EMOJI_LIMIT) + ); + onEmojiSelect(emoji); + onOpenChange(false); + }, + [onEmojiSelect, onOpenChange] + ); + + const mainEmojis = useMemo( + () => COMMENT_EMOJIS.filter((emoji) => !recentEmojis.includes(emoji)), + [recentEmojis] + ); + + const renderEmojiButton = (emoji: string, isRecent = false) => ( + + ); + + return ( + + + +
+ +
+
+
+ event.stopPropagation()} + onClick={(event) => event.stopPropagation()} + > +
+ {recentEmojis.length > 0 && ( +
+ {recentEmojis.map((emoji) => renderEmojiButton(emoji, true))} +
+ )} +
{mainEmojis.map((emoji) => renderEmojiButton(emoji))}
+
+
+
+ ); }; export function IssueCommentToolbar(props: Props) { @@ -83,6 +249,7 @@ export function IssueCommentToolbar(props: Props) { } = props; // State to manage active states of toolbar items const [activeStates, setActiveStates] = useState>({}); + const [isEmojiPickerOpen, setIsEmojiPickerOpen] = useState(false); // Function to update active states const updateActiveStates = useCallback(() => { @@ -125,15 +292,11 @@ export function IssueCommentToolbar(props: Props) { - - - + editorRef?.setEditorValueAtCursorPosition(emoji)} + /> {showSubmitButton && ( + ))} + + + )} ( -
- {category.label} -
- ), + CategoryHeader: categoryHeadersHidden + ? ({ ...props }) => ( +