Добавил дизайн в чате
This commit is contained in:
92
server.js
92
server.js
@@ -471,6 +471,98 @@ app.get('/api/messages/:contactId', authenticate, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get('/api/messages/:contactId', authenticate, async (req, res) => {
|
||||||
|
const userId = req.user.id;
|
||||||
|
const contactId = parseInt(req.params.contactId, 10);
|
||||||
|
try {
|
||||||
|
// Ensure table exists
|
||||||
|
await virtualWorldPool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
sender_id INT NOT NULL,
|
||||||
|
receiver_id INT NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
is_read BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
)`);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[GET /api/messages/:contactId] ensure table failed:', e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sql = `SELECT * FROM messages
|
||||||
|
WHERE (sender_id = $1 AND receiver_id = $2)
|
||||||
|
OR (sender_id = $2 AND receiver_id = $1)
|
||||||
|
ORDER BY created_at ASC`;
|
||||||
|
const messagesRes = await virtualWorldPool.query(sql, [userId, contactId]);
|
||||||
|
res.json(messagesRes.rows);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[GET /api/messages/:contactId] error:', err);
|
||||||
|
res.status(500).json({ error: 'Ошибка получения сообщений' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/messages-read/:contactId', authenticate, async (req, res) => {
|
||||||
|
const userId = req.user.id;
|
||||||
|
const contactId = parseInt(req.params.contactId, 10);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Проверяем есть ли НЕпрочитанные сообщения от этого контакта
|
||||||
|
const sql = `SELECT EXISTS (
|
||||||
|
SELECT 1 FROM messages
|
||||||
|
WHERE sender_id = $1
|
||||||
|
AND receiver_id = $2
|
||||||
|
AND is_read = false
|
||||||
|
AND sender_id != receiver_id
|
||||||
|
) as has_unread`;
|
||||||
|
|
||||||
|
const result = await virtualWorldPool.query(sql, [contactId, userId]);
|
||||||
|
|
||||||
|
// Если есть непрочитанные - возвращаем "true", иначе "false"
|
||||||
|
const hasUnread = result.rows[0].has_unread;
|
||||||
|
res.json(hasUnread ? "true" : "false");
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[GET /api/messages-read/:contactId] error:', err);
|
||||||
|
res.status(500).json({ error: 'Ошибка проверки состояния' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
app.post('/api/messages-read-true-false', authenticate, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.user.id;
|
||||||
|
const { contactId } = req.body;
|
||||||
|
|
||||||
|
if (!contactId) {
|
||||||
|
return res.status(400).json({ error: 'contactId required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем что contactId не равен ID текущего пользователя
|
||||||
|
if (parseInt(contactId) === userId) {
|
||||||
|
return res.json({ success: true, skipped: 'Нельзя отмечать свои собственные сообщения' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отмечаем сообщения как прочитанные ТОЛЬКО если отправитель ≠ получатель
|
||||||
|
await virtualWorldPool.query(
|
||||||
|
`UPDATE messages
|
||||||
|
SET is_read = true
|
||||||
|
WHERE sender_id = $1 AND receiver_id = $2
|
||||||
|
AND sender_id != receiver_id`, // Добавляем проверку
|
||||||
|
[contactId, userId]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating read status:', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
app.post('/api/messages/send', authenticate, async (req, res) => {
|
app.post('/api/messages/send', authenticate, async (req, res) => {
|
||||||
const senderId = req.user.id;
|
const senderId = req.user.id;
|
||||||
const { receiverId, message } = req.body || {};
|
const { receiverId, message } = req.body || {};
|
||||||
|
|||||||
145
src/Game.js
145
src/Game.js
@@ -1364,6 +1364,7 @@ function Game({ avatarUrl, gender }) {
|
|||||||
const [newMessage, setNewMessage] = useState("");
|
const [newMessage, setNewMessage] = useState("");
|
||||||
const [messageInterval, setMessageInterval] = useState(null);
|
const [messageInterval, setMessageInterval] = useState(null);
|
||||||
const [messages, setMessages] = useState([]);
|
const [messages, setMessages] = useState([]);
|
||||||
|
//const [readmes, setReadmes] = useState('false');
|
||||||
const [userProfile, setUserProfile] = useState(null);
|
const [userProfile, setUserProfile] = useState(null);
|
||||||
|
|
||||||
// Функция загрузки сообщений
|
// Функция загрузки сообщений
|
||||||
@@ -1371,7 +1372,29 @@ function Game({ avatarUrl, gender }) {
|
|||||||
if (!contactId) return;
|
if (!contactId) return;
|
||||||
|
|
||||||
const token = localStorage.getItem('token');
|
const token = localStorage.getItem('token');
|
||||||
|
async function markMessagesAsRead(contactId, token) {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/messages-read-true-false', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
contactId: contactId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.error('Ошибка отметки сообщений как прочитанных');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error marking as read:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 1. Загружаем сообщения
|
||||||
const res = await fetch(`/api/messages/${contactId}`, {
|
const res = await fetch(`/api/messages/${contactId}`, {
|
||||||
headers: { Authorization: `Bearer ${token}` }
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
});
|
});
|
||||||
@@ -1379,21 +1402,51 @@ function Game({ avatarUrl, gender }) {
|
|||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setMessages(data);
|
setMessages(data);
|
||||||
console.log('Сообщение загружено');
|
console.log('Сообщения загружены');
|
||||||
|
|
||||||
|
// 2. Отмечаем сообщения как прочитанные
|
||||||
|
await markMessagesAsRead(contactId, token);
|
||||||
|
|
||||||
// Прокручиваем чат вниз
|
// Прокручиваем чат вниз
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const chatContainer = document.getElementById('chatContainer');
|
const chatContainer = document.getElementById('chatContainer');
|
||||||
if (chatContainer) {
|
if (chatContainer) {
|
||||||
chatContainer.scrollTop = chatContainer.scrollHeight;
|
chatContainer.scrollTop = chatContainer.scrollHeight;
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
console.error('Ошибка загрузки сообщений');
|
console.error('Ошибка загрузки сообщений');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Ошибка сети:', err);
|
console.error('Ошибка:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* async function readmessages(contactId) {
|
||||||
|
if (!contactId) return;
|
||||||
|
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/messages-read/${contactId}`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
const data = await res.text();
|
||||||
|
if (data == "true") {
|
||||||
|
readmes('true'); // Есть непрочитанные
|
||||||
|
} else {
|
||||||
|
readmes('false'); // Нет непрочитанных
|
||||||
|
}
|
||||||
|
console.log('Статус прочитанности проверен:', data);
|
||||||
|
} else {
|
||||||
|
console.error('Ошибка проверки сообщений');
|
||||||
|
readmes('false');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Ошибка:', err);
|
||||||
|
readmes('false');
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
// Функция отправки сообщения
|
// Функция отправки сообщения
|
||||||
async function sendMessage() {
|
async function sendMessage() {
|
||||||
@@ -1431,10 +1484,11 @@ function Game({ avatarUrl, gender }) {
|
|||||||
if (activeChat) {
|
if (activeChat) {
|
||||||
// Первоначальная загрузка сообщений
|
// Первоначальная загрузка сообщений
|
||||||
loadMessages(activeChat.id);
|
loadMessages(activeChat.id);
|
||||||
|
//readmessages(activeChat.id)
|
||||||
// Запускаем интервал для проверки новых сообщений
|
// Запускаем интервал для проверки новых сообщений
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
loadMessages(activeChat.id);
|
loadMessages(activeChat.id);
|
||||||
|
//readmessages(activeChat.id);
|
||||||
}, 1000); // Проверка каждую секунду
|
}, 1000); // Проверка каждую секунду
|
||||||
|
|
||||||
setMessageInterval(interval);
|
setMessageInterval(interval);
|
||||||
@@ -5588,39 +5642,70 @@ function Game({ avatarUrl, gender }) {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{/* Область чата */}
|
{/* Область чата */}
|
||||||
<div style={{ flex: 1, display: isPhoneNarrow && !activeChat ? 'none' : 'flex', flexDirection: 'column', background: '#fff' }}>
|
<div style={{ flex: 1, display: isPhoneNarrow && !activeChat ? 'none' : 'flex', flexDirection: 'column', background: '#fff', overflowX: 'hidden', overflowY: 'auto' }}>
|
||||||
{activeChat && (
|
{activeChat && (
|
||||||
<>
|
<>
|
||||||
<div style={{ padding: '8px 12px', borderBottom: '1px solid #eee', display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ padding: '8px 12px', borderBottom: '1px solid #eee', display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
{isPhoneNarrow && (
|
{isPhoneNarrow && (
|
||||||
<button onClick={() => setActiveChat(null)} style={{ border: 'none', background: 'transparent', fontSize: 16, cursor: 'pointer' }}>←</button>
|
<button onClick={() => setActiveChat(null)} style={{ border: 'none', background: 'transparent', fontSize: 16, cursor: 'pointer' }}>←</button>
|
||||||
)}
|
)}
|
||||||
<span style={{ fontWeight: 600 }}>{activeChat.firstName} {activeChat.lastName}</span>
|
<span style={{ fontWeight: 600 }}>{activeChat.firstName} {activeChat.lastName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="chatContainer" style={{ flex: 1, overflowY: 'auto', padding: 10, background: '#fafafa' }}>
|
<div id="chatContainer" style={{ flex: 1, overflowY: 'auto', padding: 10, background: '#fafafa' }}>
|
||||||
{messages.length === 0 ? (
|
{console.log("UserProfile ID:", activeChat.id, "Type:", typeof activeChat.id)}
|
||||||
<p style={{ textAlign: 'center', color: '#666' }}>Нет сообщений</p>
|
{console.log("First message sender_id:", messages[0]?.sender_id, "Type:", typeof messages[0]?.sender_id)}
|
||||||
) : (
|
{messages.length === 0 ? (
|
||||||
messages.map(msg => (
|
<p style={{ textAlign: 'center', color: '#666' }}>Нет сообщений</p>
|
||||||
<div key={msg.id} style={{ display: 'flex', justifyContent: (msg.sender_id === userProfile?.id) ? 'flex-end' : 'flex-start', margin: '8px 0' }}>
|
) : (
|
||||||
<div style={{ maxWidth: '75%', background: (msg.sender_id === userProfile?.id) ? '#0084ff' : '#e5e5ea', color: (msg.sender_id === userProfile?.id) ? '#fff' : '#000', padding: '8px 12px', borderRadius: 12 }}>{msg.message}</div>
|
messages.map(msg => (
|
||||||
</div>
|
<div key={msg.id} style={{ display: 'flex', justifyContent: (Number(msg.sender_id) == Number(activeChat.id)) ? 'flex-start' : 'flex-end', margin: '8px 0' }}>
|
||||||
))
|
<div style={{
|
||||||
)}
|
maxWidth: 'min(40%, 30ch)', // Ограничение и по % и по символам
|
||||||
</div>
|
background: (Number(msg.sender_id) == Number(activeChat.id)) ? '#e5e5ea' : '#0084ff',
|
||||||
<div style={{ padding: 8, display: 'flex', gap: 8, borderTop: '1px solid #eee', background: '#fff' }}>
|
color: (Number(msg.sender_id) == Number(activeChat.id)) ? '#000' : '#fff',
|
||||||
|
padding: '8px 12px',
|
||||||
|
borderRadius: 12,
|
||||||
|
wordWrap: 'break-word',
|
||||||
|
overflowWrap: 'break-word',
|
||||||
|
whiteSpace: 'pre-wrap'
|
||||||
|
}}>
|
||||||
|
{msg.message}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div style={{
|
||||||
|
padding: 8,
|
||||||
|
display: 'flex',
|
||||||
|
gap: 8,
|
||||||
|
borderTop: '1px solid #eee',
|
||||||
|
background: '#fff',
|
||||||
|
width: '100%'
|
||||||
|
}}>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={newMessage}
|
value={newMessage}
|
||||||
onChange={(e) => setNewMessage(e.target.value)}
|
onChange={(e) => setNewMessage(e.target.value)}
|
||||||
placeholder="Сообщение"
|
placeholder="Сообщение"
|
||||||
onKeyDown={(e) => { if (e.key === 'Enter') sendMessage(); }}
|
onKeyDown={(e) => { if (e.key === 'Enter') sendMessage(); }}
|
||||||
style={{ flex: 1, width: '80%', padding: '8px 8px', borderRadius: 12, border: '1px solid #ddd' }} />
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
padding: '8px 8px',
|
||||||
|
width: '80%',
|
||||||
|
maxWidth: '140px',
|
||||||
|
borderRadius: 12,
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
wordWrap: 'break-word',
|
||||||
|
whiteSpace: 'pre-wrap'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<button onClick={sendMessage} style={{ padding: '8px 8px', background: '#0084ff', color: '#fff', border: 'none', borderRadius: 12, cursor: 'pointer' }}>➤</button>
|
<button onClick={sendMessage} style={{ padding: '8px 8px', background: '#0084ff', color: '#fff', border: 'none', borderRadius: 12, cursor: 'pointer' }}>➤</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!activeChat && (
|
{!activeChat && (
|
||||||
<div style={{ margin: 'auto', color: '#666' }}>Выберите контакт</div>
|
<div style={{ margin: 'auto', color: '#666' }}>Выберите контакт</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user