feat: GIF picker and kaomoji in DMs
- Added GiphyPicker to DM input area (toggle with [GIF] button) - Added EmojiPicker/kaomoji to DMs (toggle with [☺] or Ctrl+E) - GiphyPicker shows above input when toggled - Kaomoji appends to input text
This commit is contained in:
@@ -4,6 +4,8 @@ import { useConversationStore, type ConversationMessage } from "../stores/conver
|
|||||||
import { useAuthStore } from "../stores/auth.ts";
|
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 { EmojiPicker } from "./EmojiPicker.tsx";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
|
|
||||||
@@ -71,6 +73,8 @@ export function DMChat() {
|
|||||||
const sendTypingStart = useTypingStore((s) => s.sendTypingStart);
|
const sendTypingStart = useTypingStore((s) => s.sendTypingStart);
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [showGifPicker, setShowGifPicker] = useState(false);
|
||||||
|
const [showKaomoji, setShowKaomoji] = useState(false);
|
||||||
const bottomRef = useRef<HTMLDivElement>(null);
|
const bottomRef = useRef<HTMLDivElement>(null);
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const lastTypingRef = useRef<number>(0);
|
const lastTypingRef = useRef<number>(0);
|
||||||
@@ -108,6 +112,29 @@ export function DMChat() {
|
|||||||
bottomRef.current?.scrollIntoView({ behavior: "auto" });
|
bottomRef.current?.scrollIntoView({ behavior: "auto" });
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
|
// Ctrl+E kaomoji shortcut
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
if (e.ctrlKey && e.key === 'e') {
|
||||||
|
e.preventDefault();
|
||||||
|
setShowKaomoji((prev) => !prev);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', handler);
|
||||||
|
return () => window.removeEventListener('keydown', handler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleGifSelect = async (gif: Gif) => {
|
||||||
|
if (!id) return;
|
||||||
|
const content = ``;
|
||||||
|
try {
|
||||||
|
await sendMessage(id, content);
|
||||||
|
setShowGifPicker(false);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Failed to send");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!id || !input.trim()) return;
|
if (!id || !input.trim()) return;
|
||||||
@@ -152,6 +179,11 @@ export function DMChat() {
|
|||||||
))}
|
))}
|
||||||
<div ref={bottomRef} />
|
<div ref={bottomRef} />
|
||||||
</div>
|
</div>
|
||||||
|
{showGifPicker && (
|
||||||
|
<div className="px-3 pb-1">
|
||||||
|
<GiphyPicker onSelect={handleGifSelect} onClose={() => setShowGifPicker(false)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="px-3 pt-1 text-xs text-gb-fg-f font-mono italic h-5 select-none">
|
<div className="px-3 pt-1 text-xs text-gb-fg-f font-mono italic h-5 select-none">
|
||||||
{(() => {
|
{(() => {
|
||||||
const convTyping = id ? (typingUsers[id] || []).filter(u => u.userId !== currentUser?.id) : [];
|
const convTyping = id ? (typingUsers[id] || []).filter(u => u.userId !== currentUser?.id) : [];
|
||||||
@@ -165,7 +197,7 @@ export function DMChat() {
|
|||||||
<p className="text-gb-red text-xs font-mono">ERR: {error}</p>
|
<p className="text-gb-red text-xs font-mono">ERR: {error}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<form onSubmit={handleSubmit} className="p-3 flex items-center gap-2">
|
<form onSubmit={handleSubmit} className="p-3 flex items-center gap-2 relative">
|
||||||
<span className="text-gb-fg-f select-none shrink-0">{">"}</span>
|
<span className="text-gb-fg-f select-none shrink-0">{">"}</span>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -184,6 +216,30 @@ export function DMChat() {
|
|||||||
className="terminal-input w-full"
|
className="terminal-input w-full"
|
||||||
disabled={!id}
|
disabled={!id}
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowGifPicker((prev) => !prev)}
|
||||||
|
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 GIF picker"
|
||||||
|
>
|
||||||
|
[GIF]
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowKaomoji((prev) => !prev)}
|
||||||
|
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 kaomoji picker (Ctrl+E)"
|
||||||
|
>
|
||||||
|
[☺]
|
||||||
|
</button>
|
||||||
|
{showKaomoji && (
|
||||||
|
<EmojiPicker
|
||||||
|
onSelect={(emoji) => setInput((prev) => prev + emoji)}
|
||||||
|
onClose={() => setShowKaomoji(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user