174 lines
5.8 KiB
TypeScript
174 lines
5.8 KiB
TypeScript
import { ArrowRight, Loader2, Reply, SmilePlus, X } from "lucide-react";
|
||
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||
import { useRef, useState } from "react";
|
||
import EmojiPicker from "./EmojiPicker";
|
||
import type { MessageList } from "../../../src/api/types/models";
|
||
|
||
// Message input component
|
||
const MessageInput = ({
|
||
onSend,
|
||
replyToMessage,
|
||
onCancelReply,
|
||
messageText,
|
||
setMessageText,
|
||
isSendingMessage,
|
||
setReplyToMessage,
|
||
}: {
|
||
onSend: (text: string) => void;
|
||
replyToMessage: MessageList | undefined;
|
||
onCancelReply: () => void;
|
||
inputRef: React.RefObject<HTMLInputElement>;
|
||
messageText: string;
|
||
setMessageText: React.Dispatch<React.SetStateAction<string>>;
|
||
isSendingMessage: boolean;
|
||
setReplyToMessage: (message: MessageList | undefined) => void;
|
||
}) => {
|
||
const [caretPosition, setCaretPosition] = useState<number | null>(null);
|
||
// Track current textarea height
|
||
|
||
const handleSend = () => {
|
||
if (!messageText.trim() || isSendingMessage) return;
|
||
onSend(messageText.trim());
|
||
};
|
||
|
||
// Handle emoji selection
|
||
const handleSelectEmoji = (selectedEmoji: string) => {
|
||
if (textareaRef.current) {
|
||
// Get current cursor position
|
||
const cursorPosition =
|
||
caretPosition !== null
|
||
? caretPosition
|
||
: textareaRef.current.selectionStart;
|
||
|
||
// Insert emoji at cursor position
|
||
const newText =
|
||
messageText.substring(0, cursorPosition) +
|
||
selectedEmoji +
|
||
messageText.substring(cursorPosition);
|
||
|
||
setMessageText(newText);
|
||
|
||
// After state update, set cursor position after the inserted emoji
|
||
setTimeout(() => {
|
||
if (textareaRef.current) {
|
||
const newPosition = cursorPosition + selectedEmoji.length;
|
||
textareaRef.current.focus();
|
||
textareaRef.current.setSelectionRange(newPosition, newPosition);
|
||
}
|
||
}, 0);
|
||
} else {
|
||
// Fallback if ref not available
|
||
setMessageText((prev) => prev + selectedEmoji);
|
||
}
|
||
};
|
||
|
||
// Store caret position on selection change
|
||
const handleTextareaSelect = () => {
|
||
if (textareaRef.current) {
|
||
setCaretPosition(textareaRef.current.selectionStart);
|
||
}
|
||
};
|
||
|
||
// Auto-growing textarea ref
|
||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||
|
||
// Handle text change and update height
|
||
const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||
setMessageText(e.target.value);
|
||
};
|
||
|
||
return (
|
||
<>
|
||
{/* Reply indicator */}
|
||
{replyToMessage && (
|
||
<div className="max-w-md mx-auto bg-GRAY1 p-2 mb-2 rounded-lg flex items-center">
|
||
<div className="flex-1 text-GRAY overflow-hidden">
|
||
<p className="text-R12">در پاسخ به:</p>
|
||
<p className="text-R14 truncate">{replyToMessage.content}</p>
|
||
</div>
|
||
<button onClick={() => setReplyToMessage(undefined)} className="ml-2">
|
||
<X className="h-5 w-5 text-GRAY" />
|
||
</button>
|
||
</div>
|
||
)}
|
||
<div className="max-w-md mx-auto fixed px-2 bottom-0 py-2 w-full bg-WHITE">
|
||
{replyToMessage && (
|
||
<div className="bg-GRAY3 rounded-lg p-2 flex justify-between items-center mb-2">
|
||
<div className="flex items-center">
|
||
<Reply className="w-4 h-4 mr-2" />
|
||
<span className="text-sm truncate max-w-[250px]">
|
||
{replyToMessage.content}
|
||
</span>
|
||
</div>
|
||
<button
|
||
onClick={onCancelReply}
|
||
className="text-GRAY hover:text-BLACK2"
|
||
aria-label="حذف پاسخ"
|
||
>
|
||
<X className="w-4 h-4" />
|
||
</button>
|
||
</div>
|
||
)}
|
||
|
||
<div className="relative rounded-[12px] bg-WHITE3 flex items-end">
|
||
<Popover>
|
||
<PopoverTrigger asChild>
|
||
<button
|
||
className="absolute right-3 bottom-3 w-6 h-6 flex items-center justify-center"
|
||
aria-label="انتخاب ایموجی"
|
||
>
|
||
<SmilePlus className="text-GRAY hover:text-BLACK" size={20} />
|
||
</button>
|
||
</PopoverTrigger>
|
||
<PopoverContent
|
||
align="start"
|
||
side="top"
|
||
className="w-[280px] p-0"
|
||
sideOffset={5}
|
||
>
|
||
<EmojiPicker onSelectEmoji={handleSelectEmoji} />
|
||
</PopoverContent>
|
||
</Popover>
|
||
|
||
<textarea
|
||
ref={textareaRef}
|
||
value={messageText}
|
||
onChange={handleTextChange}
|
||
onSelect={handleTextareaSelect}
|
||
placeholder={
|
||
replyToMessage
|
||
? "پاسخ خود را بنویسید..."
|
||
: "پیام خود را بنویسید..."
|
||
}
|
||
className="hide-scrollbar overflow-y-auto h-12 flex w-full rounded-[12px] border border-input bg-transparent px-3 pr-10 py-2 text-base shadow-sm resize-none outline-none transition-height duration-100"
|
||
style={{
|
||
paddingRight: "40px",
|
||
paddingLeft: "48px",
|
||
direction: "rtl",
|
||
}}
|
||
onKeyDown={(e) => {
|
||
if (e.key === "Enter" && !e.shiftKey) {
|
||
e.preventDefault();
|
||
handleSend();
|
||
}
|
||
}}
|
||
/>
|
||
<button
|
||
onClick={handleSend}
|
||
disabled={!messageText.trim()}
|
||
className="absolute left-3 bottom-2 p-2 bg-BLUE2 text-WHITE rounded-full flex items-center justify-center disabled:opacity-50"
|
||
aria-label="ارسال پیام"
|
||
>
|
||
{isSendingMessage ? (
|
||
<Loader2 className="w-4 h-4 animate-spin" />
|
||
) : (
|
||
<ArrowRight className="w-4 h-4 transform rotate-180" />
|
||
)}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
};
|
||
export default MessageInput;
|