feat(chat): add native emoji pickers for chat input and reactions
This commit is contained in:
Generated
+22
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user