feat(chat): add native emoji pickers for chat input and reactions

This commit is contained in:
2026-07-06 13:59:20 +00:00
parent 8bbe2a0ff0
commit 9512dedec1
4 changed files with 110 additions and 8 deletions
+22
View File
@@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@livekit/components-react": "^2.9.21", "@livekit/components-react": "^2.9.21",
"@livekit/track-processors": "^0.7.2", "@livekit/track-processors": "^0.7.2",
"emoji-picker-react": "^4.19.1",
"livekit-client": "^2.20.0", "livekit-client": "^2.20.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@@ -1862,6 +1863,21 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/emoji-picker-react": {
"version": "4.19.1",
"resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.19.1.tgz",
"integrity": "sha512-BmDdqInKFVYJpv7qS9WI6L9656cDAC+FkDvUjJds56nKHbaVTBNeDmLwKBytRnzu37zWHs9Isg7gt5PT43y6xA==",
"license": "MIT",
"dependencies": {
"flairup": "1.0.0"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"react": ">=16"
}
},
"node_modules/es-errors": { "node_modules/es-errors": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
@@ -2011,6 +2027,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/flairup": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz",
"integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==",
"license": "MIT"
},
"node_modules/fraction.js": { "node_modules/fraction.js": {
"version": "5.3.4", "version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+1
View File
@@ -12,6 +12,7 @@
"dependencies": { "dependencies": {
"@livekit/components-react": "^2.9.21", "@livekit/components-react": "^2.9.21",
"@livekit/track-processors": "^0.7.2", "@livekit/track-processors": "^0.7.2",
"emoji-picker-react": "^4.19.1",
"livekit-client": "^2.20.0", "livekit-client": "^2.20.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
+59 -6
View File
@@ -20,7 +20,8 @@ import { useContextMenu } from "./ContextMenu.tsx";
import { PinnedMessages } from "./PinnedMessages.tsx"; import { PinnedMessages } from "./PinnedMessages.tsx";
import { FeatureRequestsPanel } from "./FeatureRequestsPanel.tsx"; import { FeatureRequestsPanel } from "./FeatureRequestsPanel.tsx";
import { ReactionBar } from "./ReactionBar.tsx"; import { ReactionBar } from "./ReactionBar.tsx";
import { EmojiPicker } from "./EmojiPicker.tsx"; import { EmojiPicker as KaomojiPicker } from "./EmojiPicker.tsx";
import Picker, { Theme } from 'emoji-picker-react';
import { FormatToolbar } from "./FormatToolbar.tsx"; import { FormatToolbar } from "./FormatToolbar.tsx";
import { ReplyBar } from "./ReplyBar.tsx"; import { ReplyBar } from "./ReplyBar.tsx";
import { useLayoutStore } from "../stores/layout.ts"; import { useLayoutStore } from "../stores/layout.ts";
@@ -160,6 +161,8 @@ const MessageItem = memo(({
currentUserId, currentUserId,
activeReactionMessageId, activeReactionMessageId,
setActiveReactionMessageId, setActiveReactionMessageId,
activeNativeReactionMessageId,
setActiveNativeReactionMessageId,
members, members,
parentMessage, parentMessage,
onJumpToParent, onJumpToParent,
@@ -175,6 +178,8 @@ const MessageItem = memo(({
currentUserId?: string; currentUserId?: string;
activeReactionMessageId: string | null; activeReactionMessageId: string | null;
setActiveReactionMessageId: (id: string | null) => void; setActiveReactionMessageId: (id: string | null) => void;
activeNativeReactionMessageId: string | null;
setActiveNativeReactionMessageId: (id: string | null) => void;
members: any[]; members: any[];
parentMessage: any | null; parentMessage: any | null;
onJumpToParent: (id: string) => void; onJumpToParent: (id: string) => void;
@@ -243,11 +248,22 @@ const MessageItem = memo(({
{/* Kaomoji picker popover */} {/* Kaomoji picker popover */}
{activeReactionMessageId === message.id && ( {activeReactionMessageId === message.id && (
<EmojiPicker <KaomojiPicker
onSelect={(emoji) => onAddReaction(message.id, emoji)} onSelect={(emoji) => onAddReaction(message.id, emoji)}
onClose={() => setActiveReactionMessageId(null)} onClose={() => setActiveReactionMessageId(null)}
/> />
)} )}
{activeNativeReactionMessageId === message.id && (
<div className="absolute z-50">
<Picker
theme={Theme.DARK}
onEmojiClick={(emoji) => {
onAddReaction(message.id, emoji.emoji);
setActiveNativeReactionMessageId(null);
}}
/>
</div>
)}
</div> </div>
); );
}); });
@@ -274,6 +290,7 @@ export function ChatArea() {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [showGifPicker, setShowGifPicker] = useState(false); const [showGifPicker, setShowGifPicker] = useState(false);
const [showKaomoji, setShowKaomoji] = useState(false); const [showKaomoji, setShowKaomoji] = useState(false);
const [showNativeEmoji, setShowNativeEmoji] = useState(false);
const [showSearch, setShowSearch] = useState(false); const [showSearch, setShowSearch] = useState(false);
const [mentionQuery, setMentionQuery] = useState<string | null>(null); const [mentionQuery, setMentionQuery] = useState<string | null>(null);
const [commandQuery, setCommandQuery] = useState<string | null>(null); const [commandQuery, setCommandQuery] = useState<string | null>(null);
@@ -313,6 +330,7 @@ export function ChatArea() {
const [showPinned, setShowPinned] = useState(false); const [showPinned, setShowPinned] = useState(false);
const [showFeatureRequests, setShowFeatureRequests] = useState(false); const [showFeatureRequests, setShowFeatureRequests] = useState(false);
const [activeReactionMessageId, setActiveReactionMessageId] = useState<string | null>(null); const [activeReactionMessageId, setActiveReactionMessageId] = useState<string | null>(null);
const [activeNativeReactionMessageId, setActiveNativeReactionMessageId] = useState<string | null>(null);
const [replyToMessage, setReplyToMessage] = useState<any | null>(null); const [replyToMessage, setReplyToMessage] = useState<any | null>(null);
const pinMessage = useMessageStore((s) => s.pinMessage); const pinMessage = useMessageStore((s) => s.pinMessage);
const unpinMessage = useMessageStore((s) => s.unpinMessage); const unpinMessage = useMessageStore((s) => s.unpinMessage);
@@ -337,6 +355,7 @@ export function ChatArea() {
try { try {
await api.post(`/messages/${messageId}/reactions`, { emoji }); await api.post(`/messages/${messageId}/reactions`, { emoji });
setActiveReactionMessageId(null); setActiveReactionMessageId(null);
setActiveNativeReactionMessageId(null);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to add reaction"); setError(err instanceof Error ? err.message : "Failed to add reaction");
} }
@@ -352,10 +371,18 @@ export function ChatArea() {
} }
}, },
{ {
label: "[ADD REACTION]", label: "[ADD KAOMOJI REACTION]",
onClick: () => { onClick: () => {
setActiveReactionMessageId(msg.id); setActiveReactionMessageId(msg.id);
} setActiveNativeReactionMessageId(null);
},
},
{
label: "[ADD EMOJI REACTION]",
onClick: () => {
setActiveNativeReactionMessageId(msg.id);
setActiveReactionMessageId(null);
},
}, },
{ {
label: msg.pinned ? "[UNPIN MESSAGE]" : "[PIN MESSAGE]", label: msg.pinned ? "[UNPIN MESSAGE]" : "[PIN MESSAGE]",
@@ -851,6 +878,8 @@ export function ChatArea() {
currentUserId={currentUser?.id} currentUserId={currentUser?.id}
activeReactionMessageId={activeReactionMessageId} activeReactionMessageId={activeReactionMessageId}
setActiveReactionMessageId={setActiveReactionMessageId} setActiveReactionMessageId={setActiveReactionMessageId}
activeNativeReactionMessageId={activeNativeReactionMessageId}
setActiveNativeReactionMessageId={setActiveNativeReactionMessageId}
members={members} members={members}
parentMessage={message.reply_to ? messages.find((m) => m.id === message.reply_to) : null} parentMessage={message.reply_to ? messages.find((m) => m.id === message.reply_to) : null}
onJumpToParent={handleJumpToMessage} onJumpToParent={handleJumpToMessage}
@@ -934,21 +963,45 @@ export function ChatArea() {
> >
[GIF] [GIF]
</button> </button>
<button
type="button"
onClick={() => {
setShowNativeEmoji((prev) => !prev);
setShowKaomoji(false);
setShowGifPicker(false);
}}
disabled={!activeChannelId || slowmodeRemaining > 0}
className="text-gb-fg-f hover:text-gb-orange font-mono text-sm select-none disabled:opacity-50 disabled:cursor-not-allowed shrink-0"
title="Toggle native emoji picker"
>
[E]
</button>
<button <button
type="button" type="button"
onClick={() => setShowKaomoji((prev) => !prev)} onClick={() => setShowKaomoji((prev) => !prev)}
disabled={!activeChannelId} disabled={!activeChannelId || slowmodeRemaining > 0}
className="text-gb-fg-f hover:text-gb-orange font-mono text-sm select-none disabled:opacity-50 disabled:cursor-not-allowed shrink-0" className="text-gb-fg-f hover:text-gb-orange font-mono text-sm select-none disabled:opacity-50 disabled:cursor-not-allowed shrink-0"
title="Toggle kaomoji picker (Ctrl+E)" title="Toggle kaomoji picker (Ctrl+E)"
> >
[] []
</button> </button>
{showKaomoji && ( {showKaomoji && (
<EmojiPicker <KaomojiPicker
onSelect={(emoji) => setInput((prev) => prev + emoji)} onSelect={(emoji) => setInput((prev) => prev + emoji)}
onClose={() => setShowKaomoji(false)} onClose={() => setShowKaomoji(false)}
/> />
)} )}
{showNativeEmoji && (
<div className="absolute bottom-full right-0 mb-2 z-50">
<Picker
theme={Theme.DARK}
onEmojiClick={(emoji) => {
setInput((prev) => prev + emoji.emoji);
setShowNativeEmoji(false);
}}
/>
</div>
)}
</form> </form>
</div> </div>
{activeThread && ( {activeThread && (
+28 -2
View File
@@ -5,7 +5,8 @@ import { useAuthStore } from "../stores/auth.ts";
import { useTypingStore } from "../stores/typing.ts"; import { useTypingStore } from "../stores/typing.ts";
import { useLayoutStore } from "../stores/layout.ts"; import { useLayoutStore } from "../stores/layout.ts";
import { GiphyPicker, type Gif } from "./GiphyPicker.tsx"; import { GiphyPicker, type Gif } from "./GiphyPicker.tsx";
import { EmojiPicker } from "./EmojiPicker.tsx"; import { EmojiPicker as KaomojiPicker } from "./EmojiPicker.tsx";
import Picker, { Theme } from 'emoji-picker-react';
import { FormatToolbar } from "./FormatToolbar.tsx"; import { FormatToolbar } from "./FormatToolbar.tsx";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
@@ -76,6 +77,7 @@ export function DMChat() {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [showGifPicker, setShowGifPicker] = useState(false); const [showGifPicker, setShowGifPicker] = useState(false);
const [showKaomoji, setShowKaomoji] = useState(false); const [showKaomoji, setShowKaomoji] = useState(false);
const [showNativeEmoji, setShowNativeEmoji] = useState(false);
const bottomRef = useRef<HTMLDivElement>(null); const bottomRef = useRef<HTMLDivElement>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null); const scrollContainerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@@ -282,6 +284,19 @@ export function DMChat() {
> >
[GIF] [GIF]
</button> </button>
<button
type="button"
onClick={() => {
setShowNativeEmoji((prev) => !prev);
setShowKaomoji(false);
setShowGifPicker(false);
}}
disabled={!id}
className="text-gb-fg-f hover:text-gb-orange font-mono text-sm select-none disabled:opacity-50 disabled:cursor-not-allowed shrink-0"
title="Toggle native emoji picker"
>
[E]
</button>
<button <button
type="button" type="button"
onClick={() => setShowKaomoji((prev) => !prev)} onClick={() => setShowKaomoji((prev) => !prev)}
@@ -292,11 +307,22 @@ export function DMChat() {
[] []
</button> </button>
{showKaomoji && ( {showKaomoji && (
<EmojiPicker <KaomojiPicker
onSelect={(emoji) => setInput((prev) => prev + emoji)} onSelect={(emoji) => setInput((prev) => prev + emoji)}
onClose={() => setShowKaomoji(false)} onClose={() => setShowKaomoji(false)}
/> />
)} )}
{showNativeEmoji && (
<div className="absolute bottom-full right-0 mb-2 z-50">
<Picker
theme={Theme.DARK}
onEmojiClick={(emoji) => {
setInput((prev) => prev + emoji.emoji);
setShowNativeEmoji(false);
}}
/>
</div>
)}
</form> </form>
</div> </div>
); );