2026-01-07 09:32:53 +00:00
|
|
|
import React, { useState, lazy, Suspense } from 'react';
|
2025-12-03 21:56:50 +00:00
|
|
|
import { ChatMessage } from '../store/chatStore';
|
2025-12-26 08:11:41 +00:00
|
|
|
import { useAuthStore } from '@/features/auth/store/authStore';
|
2025-12-03 21:56:50 +00:00
|
|
|
import { cn } from '@/lib/utils';
|
2026-01-11 02:20:52 +00:00
|
|
|
import { Smile, MoreHorizontal, Check, CheckCheck } from 'lucide-react';
|
2026-01-04 00:41:51 +00:00
|
|
|
import { useChat } from '../hooks/useChat';
|
2026-01-07 09:32:53 +00:00
|
|
|
import { LoadingSpinner } from '@/components/ui/loading-spinner';
|
2026-01-11 02:20:52 +00:00
|
|
|
import { Theme } from 'emoji-picker-react';
|
|
|
|
|
|
|
|
|
|
const EmojiPicker = lazy(() => import('emoji-picker-react').then(module => ({ default: module.default })));
|
2025-12-03 21:56:50 +00:00
|
|
|
|
|
|
|
|
interface ChatMessageProps {
|
|
|
|
|
message: ChatMessage;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 02:34:34 +00:00
|
|
|
export const ChatMessageComponent: React.FC<ChatMessageProps> = ({
|
|
|
|
|
message,
|
|
|
|
|
}) => {
|
2025-12-03 21:56:50 +00:00
|
|
|
const { user } = useAuthStore();
|
2026-01-04 00:41:51 +00:00
|
|
|
const { addReaction } = useChat();
|
2025-12-25 13:27:28 +00:00
|
|
|
const isMe = user?.id === message.sender_id;
|
2026-01-04 00:41:51 +00:00
|
|
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
|
|
|
|
|
|
|
|
|
const handleEmojiClick = (emojiData: { emoji: string }) => {
|
|
|
|
|
addReaction(message.id, emojiData.emoji);
|
|
|
|
|
setShowEmojiPicker(false);
|
|
|
|
|
};
|
2025-12-03 21:56:50 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className={cn(
|
2026-01-11 02:20:52 +00:00
|
|
|
'group flex flex-col gap-1 max-w-[80%] mb-4 relative',
|
2026-01-04 00:41:51 +00:00
|
|
|
isMe ? 'ml-auto items-end' : 'mr-auto items-start',
|
2025-12-03 21:56:50 +00:00
|
|
|
)}
|
|
|
|
|
>
|
2026-01-11 02:20:52 +00:00
|
|
|
<div className="flex items-center gap-2 px-1 mb-0.5">
|
|
|
|
|
<span className={cn(
|
|
|
|
|
"font-mono text-[10px] uppercase tracking-wider",
|
|
|
|
|
isMe ? "text-kodo-cyan" : "text-kodo-magenta"
|
|
|
|
|
)}>
|
|
|
|
|
{isMe ? 'You' : message.sender_username || 'Unknown_Signal'}
|
2026-01-04 00:41:51 +00:00
|
|
|
</span>
|
2026-01-11 02:20:52 +00:00
|
|
|
<span className="text-[9px] text-kodo-secondary/60">
|
2026-01-04 00:41:51 +00:00
|
|
|
{new Date(message.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-11 02:20:52 +00:00
|
|
|
<div className="relative flex items-end gap-2 group/bubble">
|
|
|
|
|
{/* Emoji Button (Left for Me) */}
|
2026-01-04 00:41:51 +00:00
|
|
|
{isMe && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
2026-01-11 02:20:52 +00:00
|
|
|
className="opacity-0 group-hover/bubble:opacity-100 p-1.5 hover:bg-white/10 rounded-full transition-all text-kodo-secondary hover:text-kodo-cyan"
|
2026-01-04 00:41:51 +00:00
|
|
|
>
|
2026-01-11 02:20:52 +00:00
|
|
|
<Smile size={14} />
|
2026-01-04 00:41:51 +00:00
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-01-11 02:20:52 +00:00
|
|
|
{/* Message Bubble */}
|
2026-01-04 00:41:51 +00:00
|
|
|
<div
|
|
|
|
|
className={cn(
|
2026-01-11 02:20:52 +00:00
|
|
|
'px-4 py-2.5 rounded-2xl text-sm backdrop-blur-md shadow-lg transition-all',
|
2026-01-04 00:41:51 +00:00
|
|
|
isMe
|
2026-01-11 02:20:52 +00:00
|
|
|
? 'bg-kodo-cyan/10 border border-kodo-cyan/20 text-white rounded-tr-sm shadow-[0_0_15px_rgba(102,252,241,0.05)]'
|
|
|
|
|
: 'bg-white/5 border border-white/10 text-gray-100 rounded-tl-sm hover:bg-white/10',
|
2026-01-04 00:41:51 +00:00
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{/* Attachments */}
|
|
|
|
|
{message.attachments && message.attachments.length > 0 && (
|
|
|
|
|
<div className="mb-2 flex flex-wrap gap-2">
|
|
|
|
|
{message.attachments.map((att, i) => (
|
2026-01-11 02:20:52 +00:00
|
|
|
<div key={i} className="max-w-full overflow-hidden rounded-lg border border-white/10 bg-black/20">
|
2026-01-04 00:41:51 +00:00
|
|
|
{att.file_type.startsWith('image') ? (
|
|
|
|
|
<img
|
|
|
|
|
src={att.file_url}
|
|
|
|
|
alt={att.file_name}
|
2026-01-11 02:20:52 +00:00
|
|
|
className="max-h-60 object-contain cursor-pointer hover:opacity-90 transition-opacity"
|
2026-01-04 00:41:51 +00:00
|
|
|
onClick={() => window.open(att.file_url, '_blank')}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<a
|
|
|
|
|
href={att.file_url}
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
2026-01-11 02:20:52 +00:00
|
|
|
className="flex items-center gap-3 p-3 hover:bg-white/5 transition-colors"
|
2026-01-04 00:41:51 +00:00
|
|
|
>
|
2026-01-11 02:20:52 +00:00
|
|
|
<div className="w-8 h-8 rounded bg-white/10 flex items-center justify-center">
|
|
|
|
|
<MoreHorizontal size={16} className="text-kodo-cyan" />
|
|
|
|
|
</div>
|
|
|
|
|
<span className="truncate max-w-[150px] text-xs font-mono">{att.file_name}</span>
|
2026-01-04 00:41:51 +00:00
|
|
|
</a>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-01-11 02:20:52 +00:00
|
|
|
<p className="whitespace-pre-wrap break-words leading-relaxed">{message.content}</p>
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
2026-01-04 00:41:51 +00:00
|
|
|
|
2026-01-11 02:20:52 +00:00
|
|
|
{/* Emoji Button (Right for Others) */}
|
2026-01-04 00:41:51 +00:00
|
|
|
{!isMe && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
2026-01-11 02:20:52 +00:00
|
|
|
className="opacity-0 group-hover/bubble:opacity-100 p-1.5 hover:bg-white/10 rounded-full transition-all text-kodo-secondary hover:text-kodo-cyan"
|
2026-01-04 00:41:51 +00:00
|
|
|
>
|
2026-01-11 02:20:52 +00:00
|
|
|
<Smile size={14} />
|
2026-01-04 00:41:51 +00:00
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-01-11 02:20:52 +00:00
|
|
|
{/* Emoji Picker Popover */}
|
2026-01-04 00:41:51 +00:00
|
|
|
{showEmojiPicker && (
|
2026-01-11 02:20:52 +00:00
|
|
|
<div className={cn(
|
|
|
|
|
"absolute z-50 bottom-full mb-2",
|
|
|
|
|
isMe ? "right-0" : "left-0"
|
|
|
|
|
)}>
|
2026-01-04 00:41:51 +00:00
|
|
|
<div className="fixed inset-0" onClick={() => setShowEmojiPicker(false)} />
|
2026-01-11 02:20:52 +00:00
|
|
|
<div className="relative shadow-2xl rounded-xl overflow-hidden border border-white/10 animate-scaleIn">
|
|
|
|
|
<Suspense fallback={<div className="w-[300px] h-[400px] bg-kodo-ink flex items-center justify-center"><LoadingSpinner size="sm" /></div>}>
|
2026-01-07 09:32:53 +00:00
|
|
|
<EmojiPicker
|
|
|
|
|
onEmojiClick={handleEmojiClick}
|
2026-01-11 02:20:52 +00:00
|
|
|
theme={Theme.DARK}
|
2026-01-07 09:32:53 +00:00
|
|
|
lazyLoadEmojis={true}
|
2026-01-11 02:20:52 +00:00
|
|
|
width={300}
|
|
|
|
|
height={400}
|
2026-01-07 09:32:53 +00:00
|
|
|
/>
|
|
|
|
|
</Suspense>
|
2026-01-04 00:41:51 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
2026-01-04 00:41:51 +00:00
|
|
|
|
2026-01-11 02:20:52 +00:00
|
|
|
{/* Footer (Reactions + Status) */}
|
|
|
|
|
<div className="flex items-center justify-between w-full px-1 mt-1">
|
|
|
|
|
<div className="flex flex-wrap gap-1">
|
|
|
|
|
{message.reactions && Object.entries(message.reactions).map(([emoji, users]) => (
|
2026-01-04 00:41:51 +00:00
|
|
|
<button
|
|
|
|
|
key={emoji}
|
|
|
|
|
onClick={() => addReaction(message.id, emoji)}
|
|
|
|
|
className={cn(
|
2026-01-11 02:20:52 +00:00
|
|
|
"flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[10px] border transition-all animate-scaleIn",
|
2026-01-04 00:41:51 +00:00
|
|
|
users.includes(user?.id || '')
|
2026-01-11 02:20:52 +00:00
|
|
|
? "bg-kodo-cyan/20 border-kodo-cyan/40 text-kodo-cyan shadow-[0_0_10px_rgba(102,252,241,0.2)]"
|
|
|
|
|
: "bg-white/5 border-white/10 text-kodo-secondary hover:bg-white/10 hover:border-white/20"
|
2026-01-04 00:41:51 +00:00
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<span>{emoji}</span>
|
2026-01-11 02:20:52 +00:00
|
|
|
{users.length > 1 && <span className="font-bold">{users.length}</span>}
|
2026-01-04 00:41:51 +00:00
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
2026-01-11 02:20:52 +00:00
|
|
|
|
|
|
|
|
{isMe && (
|
|
|
|
|
<div className="text-kodo-secondary/40 ml-auto">
|
|
|
|
|
<CheckCheck size={12} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-12-03 21:56:50 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
2026-01-11 02:20:52 +00:00
|
|
|
};
|