File size: 7,576 Bytes
57da3ff | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | import { useState, useEffect, useRef } from 'react';
import { useStore } from '../store';
import { socket } from '../socket';
import { motion, AnimatePresence } from 'framer-motion';
import { X, Send, User } from 'lucide-react';
import { v4 as uuidv4 } from 'uuid';
export function ChatOverlay() {
const { activeChatUser, closeChat, currentUser } = useStore();
const [messages, setMessages] = useState([]);
const [inputText, setInputText] = useState('');
const messagesEndRef = useRef(null);
useEffect(() => {
if (!activeChatUser || !currentUser) return;
// Fetch History
fetch(`/api/messages/history/${currentUser.id}/${activeChatUser.id}`)
.then(res => res.json())
.then(data => {
setMessages(data);
return fetch(`/api/messages/read/${activeChatUser.id}/${currentUser.id}`, { method: 'PATCH' });
})
.then(() => useStore.getState().fetchUnreadCount())
.catch(console.error);
const handleReceiveMessage = (msg) => {
// Only append if it belongs to this conversation
if (msg.senderId === activeChatUser.id || msg.receiverId === activeChatUser.id) {
setMessages(prev => {
if (prev.some(m => m.id === msg.id)) return prev;
return [...prev, msg];
});
}
};
socket.on('receive-message', handleReceiveMessage);
return () => {
socket.off('receive-message', handleReceiveMessage);
};
}, [activeChatUser, currentUser]);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
const sendMessage = async (e) => {
e.preventDefault();
if (!inputText.trim() || !activeChatUser) return;
const newMsg = {
id: uuidv4(),
senderId: currentUser.id,
receiverId: activeChatUser.id,
message: inputText.trim(),
timestamp: Date.now()
};
// Optimistically update UI
setMessages(prev => [...prev, newMsg]);
setInputText('');
// Emit via socket purely for live relay
socket.emit('send-message', newMsg);
// Save strictly to database
try {
await fetch(`/api/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-socket-id': socket.id },
body: JSON.stringify(newMsg)
});
} catch(err) { console.error("Failed to send message", err); }
};
if (!activeChatUser) return null;
return (
<AnimatePresence>
<div
style={{
position: 'fixed', bottom: 0, right: 0, left: 0, top: 0,
background: 'rgba(0,0,0,0.5)', zIndex: 9999,
display: 'flex', justifyContent: 'center'
}}
onClick={closeChat} // Close if clicking backdrop
>
<motion.div
initial={{ opacity: 0, y: 100 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 100 }}
transition={{ duration: 0.2 }}
style={{
width: '100%', maxWidth: '480px', height: '100%',
background: 'var(--surface-color)', position: 'relative',
display: 'flex', flexDirection: 'column',
boxShadow: '0 0 50px rgba(0,0,0,0.8)'
}}
onClick={e => e.stopPropagation()} // Prevent closing when clicking inside
>
{/* Chat Header */}
<div style={{ padding: '16px 20px', borderBottom: '1px solid var(--border-color)', display: 'flex', alignItems: 'center', gap: '16px', background: 'var(--bg-color)' }}>
<button onClick={closeChat} style={{ background: 'var(--surface-light)', border: 'none', color: 'var(--text-main)', width: '36px', height: '36px', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}>
<X size={20} />
</button>
<div style={{ width: '40px', height: '40px', borderRadius: '50%', overflow: 'hidden', background: 'var(--surface-light)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
{activeChatUser.selfiePath ? (
<img src={activeChatUser.selfiePath} alt="Avatar" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
) : (
<User size={20} color="var(--text-muted)" />
)}
</div>
<div>
<h3 style={{ margin: 0, fontSize: '18px' }}>{activeChatUser.name}</h3>
<p style={{ margin: 0, fontSize: '13px', color: 'var(--primary-color)' }}>Active Now</p>
</div>
</div>
{/* Chat Transcript Log */}
<div style={{ flex: 1, overflowY: 'auto', padding: '20px', display: 'flex', flexDirection: 'column', gap: '12px' }}>
{messages.length === 0 ? (
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', color: 'var(--text-muted)', fontSize: '14px', textAlign: 'center' }}>
Start your conversation! <br/>Messages are end-to-end direct.
</div>
) : (
messages.map(msg => {
const isMe = msg.senderId === currentUser?.id;
return (
<div key={msg.id} style={{ alignSelf: isMe ? 'flex-end' : 'flex-start', maxWidth: '85%' }}>
<div style={{
background: isMe ? 'linear-gradient(135deg, #10b981, #059669)' : 'var(--surface-light)',
padding: '12px 16px', borderRadius: isMe ? '16px 16px 4px 16px' : '16px 16px 16px 4px',
color: 'white', fontSize: '15px', lineHeight: 1.4, wordBreak: 'break-word',
border: isMe ? 'none' : '1px solid var(--border-color)'
}}>
{msg.message}
</div>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '4px', textAlign: isMe ? 'right' : 'left' }}>
{new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
);
})
)}
<div ref={messagesEndRef} />
</div>
{/* Chat Input Dock */}
<form onSubmit={sendMessage} style={{ padding: '16px', background: 'var(--bg-color)', borderTop: '1px solid var(--border-color)', display: 'flex', gap: '10px' }}>
<input
type="text"
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="Type your message..."
style={{ flex: 1, background: 'var(--surface-light)', border: '1px solid var(--border-color)', color: 'white', padding: '12px 16px', borderRadius: '24px', fontSize: '15px', outline: 'none' }}
/>
<button type="submit" disabled={!inputText.trim()} style={{
background: 'linear-gradient(135deg, #10b981, #059669)', color: 'white', border: 'none',
width: '46px', height: '46px', borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: inputText.trim() ? 'pointer' : 'not-allowed', opacity: inputText.trim() ? 1 : 0.5,
transition: '0.2s', boxShadow: inputText.trim() ? '0 4px 15px rgba(16, 185, 129, 0.4)' : 'none'
}}>
<Send size={18} style={{ marginLeft: '4px' }} /> {/* Slight offset makes pencil/plane look centered */}
</button>
</form>
</motion.div>
</div>
</AnimatePresence>
);
}
|