|
|
@ -1,15 +1,11 @@ |
|
|
document.addEventListener('DOMContentLoaded', function () { |
|
|
document.addEventListener('DOMContentLoaded', function () { |
|
|
|
|
|
|
|
|
// متغیرهای وضعیت
|
|
|
|
|
|
let currentRoomId = null; |
|
|
let currentRoomId = null; |
|
|
let lastMessageId = 0; |
|
|
let lastMessageId = 0; |
|
|
let messagePollInterval = null; |
|
|
let messagePollInterval = null; |
|
|
let roomPollInterval = null; |
|
|
let roomPollInterval = null; |
|
|
|
|
|
|
|
|
// 🟢 متغیر قفل برای جلوگیری از دریافت پیامهای تکراری (Race Condition)
|
|
|
|
|
|
let isFetchingMessages = false; |
|
|
let isFetchingMessages = false; |
|
|
|
|
|
|
|
|
// المانهای DOM
|
|
|
|
|
|
const UI = { |
|
|
const UI = { |
|
|
roomList: document.getElementById('room-list'), |
|
|
roomList: document.getElementById('room-list'), |
|
|
noChatSelected: document.getElementById('no-chat-selected'), |
|
|
noChatSelected: document.getElementById('no-chat-selected'), |
|
|
@ -19,21 +15,119 @@ document.addEventListener('DOMContentLoaded', function () { |
|
|
inputArea: document.getElementById('chat-input-area'), |
|
|
inputArea: document.getElementById('chat-input-area'), |
|
|
form: document.getElementById('chat-form'), |
|
|
form: document.getElementById('chat-form'), |
|
|
input: document.getElementById('chat-input'), |
|
|
input: document.getElementById('chat-input'), |
|
|
indicator: document.getElementById('polling-indicator') |
|
|
|
|
|
|
|
|
indicator: document.getElementById('polling-indicator'), |
|
|
|
|
|
|
|
|
|
|
|
// 🟢 متغیرهای جدید برای Modal کاربران
|
|
|
|
|
|
newChatBtn: document.getElementById('new-chat-btn'), |
|
|
|
|
|
usersModal: document.getElementById('users-modal'), |
|
|
|
|
|
closeModalBtn: document.getElementById('close-users-modal'), |
|
|
|
|
|
usersList: document.getElementById('users-list'), |
|
|
|
|
|
searchInput: document.getElementById('user-search-input') |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
// ── منطق مودال کاربران ──
|
|
|
|
|
|
if (UI.newChatBtn) { |
|
|
|
|
|
UI.newChatBtn.addEventListener('click', () => { |
|
|
|
|
|
UI.usersModal.style.transform = 'translateY(0)'; // نمایش مودال (بالا آمدن)
|
|
|
|
|
|
fetchUsers(''); // لود اولیه لیست کاربران
|
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (UI.closeModalBtn) { |
|
|
|
|
|
UI.closeModalBtn.addEventListener('click', () => { |
|
|
|
|
|
UI.usersModal.style.transform = 'translateY(100%)'; // مخفی کردن مودال (پایین رفتن)
|
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (UI.searchInput) { |
|
|
|
|
|
let timeout = null; |
|
|
|
|
|
UI.searchInput.addEventListener('input', (e) => { |
|
|
|
|
|
clearTimeout(timeout); |
|
|
|
|
|
// دیباونس (Debounce) برای جلوگیری از ریکوئستهای تکراری
|
|
|
|
|
|
timeout = setTimeout(() => { |
|
|
|
|
|
fetchUsers(e.target.value); |
|
|
|
|
|
}, 300); |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async function fetchUsers(query) { |
|
|
|
|
|
UI.usersList.innerHTML = '<div class="flex items-center justify-center h-20"><span class="material-symbols-outlined animate-spin text-gray-400">autorenew</span></div>'; |
|
|
|
|
|
try { |
|
|
|
|
|
const res = await fetch(`${window.CHAT_CONFIG.apiUrlUsers}?q=${encodeURIComponent(query)}`); |
|
|
|
|
|
const data = await res.json(); |
|
|
|
|
|
renderUsers(data.users); |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
console.error("Failed to fetch users:", e); |
|
|
|
|
|
UI.usersList.innerHTML = '<p class="text-center text-red-400 mt-4 text-sm">Failed to load users.</p>'; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function renderUsers(users) { |
|
|
|
|
|
if (users.length === 0) { |
|
|
|
|
|
UI.usersList.innerHTML = '<p class="text-center text-gray-500 mt-6 text-sm font-medium">No users found.</p>'; |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
UI.usersList.innerHTML = users.map(user => { |
|
|
|
|
|
const initial = user.name ? user.name.charAt(0).toUpperCase() : 'U'; |
|
|
|
|
|
|
|
|
|
|
|
// استفاده از کلاسهای room-item و room-avatar برای هماهنگی 100 درصدی
|
|
|
|
|
|
return `
|
|
|
|
|
|
<div onclick="startChatWithUser(${user.id})" class="room-item flex items-center gap-3 p-3 cursor-pointer"> |
|
|
|
|
|
|
|
|
|
|
|
<div class="room-avatar" style="background: linear-gradient(135deg, #6366f1, #4f46e5); box-shadow: 0 2px 8px rgba(99,102,241,0.3);"> |
|
|
|
|
|
${initial} |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
<div class="flex-1 min-w-0"> |
|
|
|
|
|
<div class="mb-1.5"> |
|
|
|
|
|
<span class="font-semibold text-[14px] text-gray-900 dark:text-gray-100 truncate block">${user.name}</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="flex items-center"> |
|
|
|
|
|
<span class="text-[10px] font-medium tracking-wide text-gray-500 uppercase bg-gray-200 dark:bg-gray-700 px-2 py-0.5 rounded-full">${user.role}</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
</div> |
|
|
|
|
|
`;
|
|
|
|
|
|
}).join(''); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
window.startChatWithUser = async function (userId) { |
|
|
|
|
|
// بستن مودال بلافاصله بعد از کلیک
|
|
|
|
|
|
UI.usersModal.style.transform = 'translateY(100%)'; |
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
const res = await fetch(window.CHAT_CONFIG.apiUrlCreateRoom, { |
|
|
|
|
|
method: 'POST', |
|
|
|
|
|
headers: { |
|
|
|
|
|
'Content-Type': 'application/json', |
|
|
|
|
|
'X-CSRFToken': window.CHAT_CONFIG.csrfToken |
|
|
|
|
|
}, |
|
|
|
|
|
body: JSON.stringify({ user_id: userId }) |
|
|
|
|
|
}); |
|
|
|
|
|
const data = await res.json(); |
|
|
|
|
|
|
|
|
|
|
|
if (data.success) { |
|
|
|
|
|
// اگر با موفقیت ساخته یا پیدا شد، مستقیماً وارد چت میشویم
|
|
|
|
|
|
selectRoom(data.room_id, data.title); |
|
|
|
|
|
} |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
console.error("Failed to start chat:", e); |
|
|
|
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
// ── پایان منطق مودال کاربران ──
|
|
|
|
|
|
|
|
|
// گرفتن لیست چتها از سرور
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ── بقیه کدهای قبلی (fetchRooms, renderRooms, fetchMessages و...) بدون تغییر در اینجا قرار دارند ──
|
|
|
async function fetchRooms() { |
|
|
async function fetchRooms() { |
|
|
try { |
|
|
try { |
|
|
const res = await fetch(window.CHAT_CONFIG.apiUrlRooms); |
|
|
const res = await fetch(window.CHAT_CONFIG.apiUrlRooms); |
|
|
const data = await res.json(); |
|
|
const data = await res.json(); |
|
|
renderRooms(data.rooms); |
|
|
renderRooms(data.rooms); |
|
|
} catch (e) { |
|
|
|
|
|
console.error("Failed to fetch rooms:", e); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
} catch (e) { console.error("Failed to fetch rooms:", e); } |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// رندر کردن لیست چتها
|
|
|
|
|
|
function renderRooms(rooms) { |
|
|
function renderRooms(rooms) { |
|
|
if (rooms.length === 0) { |
|
|
if (rooms.length === 0) { |
|
|
UI.roomList.innerHTML = '<p class="text-center text-sm text-gray-500 mt-10 px-4">No active chats found.</p>'; |
|
|
UI.roomList.innerHTML = '<p class="text-center text-sm text-gray-500 mt-10 px-4">No active chats found.</p>'; |
|
|
@ -43,27 +137,35 @@ document.addEventListener('DOMContentLoaded', function () { |
|
|
UI.roomList.innerHTML = rooms.map(room => { |
|
|
UI.roomList.innerHTML = rooms.map(room => { |
|
|
const isActive = currentRoomId === room.id; |
|
|
const isActive = currentRoomId === room.id; |
|
|
const safeTitle = room.title.replace(/'/g, "\\'"); |
|
|
const safeTitle = room.title.replace(/'/g, "\\'"); |
|
|
|
|
|
const initial = room.title ? room.title.charAt(0).toUpperCase() : 'G'; |
|
|
|
|
|
|
|
|
return `
|
|
|
return `
|
|
|
<div onclick="selectRoom(${room.id}, '${safeTitle}')" |
|
|
<div onclick="selectRoom(${room.id}, '${safeTitle}')" |
|
|
class="group px-5 py-4 cursor-pointer border-b border-gray-200 dark:border-gray-800 transition-all duration-150 border-l-4 ${isActive ? 'room-item-active' : 'border-l-transparent hover:bg-gray-100 dark:hover:bg-gray-800/60'}"> |
|
|
|
|
|
<div class="flex justify-between items-center mb-1.5"> |
|
|
|
|
|
<span class="font-semibold text-[14px] text-gray-900 dark:text-gray-100 truncate pr-2">${room.title}</span> |
|
|
|
|
|
<span class="text-[11px] text-gray-400 whitespace-nowrap flex-shrink-0">${room.time}</span> |
|
|
|
|
|
|
|
|
class="room-item flex items-center gap-3 p-3 cursor-pointer ${isActive ? 'room-item-active' : ''}"> |
|
|
|
|
|
|
|
|
|
|
|
<div class="room-avatar"> |
|
|
|
|
|
${initial} |
|
|
</div> |
|
|
</div> |
|
|
<div class="flex justify-between items-center"> |
|
|
|
|
|
<span class="text-[10px] font-medium text-gray-500 uppercase bg-gray-200 dark:bg-gray-700 px-2 py-0.5 rounded">${room.type}</span> |
|
|
|
|
|
${room.unread > 0 ? `<span class="bg-red-500 shadow-sm text-white text-[10px] font-bold px-2 py-0.5 rounded-full">${room.unread}</span>` : ''} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="flex-1 min-w-0"> |
|
|
|
|
|
<div class="flex justify-between items-center mb-1"> |
|
|
|
|
|
<span class="font-semibold text-[14px] text-gray-900 dark:text-gray-100 truncate pr-2">${room.title}</span> |
|
|
|
|
|
<span class="text-[11px] text-gray-400 whitespace-nowrap flex-shrink-0">${room.time}</span> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="flex justify-between items-center"> |
|
|
|
|
|
<span class="text-[12px] font-medium text-gray-500 truncate">${room.type === 'group' ? '👥 Group Chat' : '👤 Private Chat'}</span> |
|
|
|
|
|
${room.unread > 0 ? `<span class="bg-blue-500 shadow-sm text-white text-[10px] font-bold px-2 py-0.5 rounded-full">${room.unread}</span>` : ''} |
|
|
|
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
`;
|
|
|
`;
|
|
|
}).join(''); |
|
|
}).join(''); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// انتخاب یک چت
|
|
|
|
|
|
window.selectRoom = function (id, title) { |
|
|
window.selectRoom = function (id, title) { |
|
|
currentRoomId = id; |
|
|
currentRoomId = id; |
|
|
lastMessageId = 0; |
|
|
lastMessageId = 0; |
|
|
isFetchingMessages = false; // ریست کردن قفل
|
|
|
|
|
|
|
|
|
isFetchingMessages = false; |
|
|
|
|
|
|
|
|
UI.noChatSelected.classList.add('hidden'); |
|
|
UI.noChatSelected.classList.add('hidden'); |
|
|
UI.chatHeader.classList.remove('invisible'); |
|
|
UI.chatHeader.classList.remove('invisible'); |
|
|
@ -84,12 +186,9 @@ document.addEventListener('DOMContentLoaded', function () { |
|
|
UI.input.focus(); |
|
|
UI.input.focus(); |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
// گرفتن پیامها از سرور
|
|
|
|
|
|
async function fetchMessages() { |
|
|
async function fetchMessages() { |
|
|
// 🟢 اگر هیچ رومی انتخاب نشده، یا درخواست قبلی هنوز تمام نشده، برگرد!
|
|
|
|
|
|
if (!currentRoomId || isFetchingMessages) return; |
|
|
if (!currentRoomId || isFetchingMessages) return; |
|
|
|
|
|
|
|
|
isFetchingMessages = true; // 🟢 فعال کردن قفل
|
|
|
|
|
|
|
|
|
isFetchingMessages = true; |
|
|
|
|
|
|
|
|
try { |
|
|
try { |
|
|
UI.indicator.classList.remove('hidden'); |
|
|
UI.indicator.classList.remove('hidden'); |
|
|
@ -105,20 +204,15 @@ document.addEventListener('DOMContentLoaded', function () { |
|
|
lastMessageId = data.messages[data.messages.length - 1].id; |
|
|
lastMessageId = data.messages[data.messages.length - 1].id; |
|
|
scrollToBottom(); |
|
|
scrollToBottom(); |
|
|
} else if (lastMessageId === 0) { |
|
|
} else if (lastMessageId === 0) { |
|
|
UI.messages.innerHTML = `
|
|
|
|
|
|
<div class="flex flex-col items-center justify-center h-full text-gray-400 opacity-60"> |
|
|
|
|
|
<span class="material-symbols-outlined text-5xl mb-2">forum</span> |
|
|
|
|
|
<p class="text-sm font-medium">No messages yet. Say hi!</p> |
|
|
|
|
|
</div>`; |
|
|
|
|
|
|
|
|
UI.messages.innerHTML = '<div class="flex flex-col items-center justify-center h-full text-gray-400 gap-2"><span class="material-symbols-outlined text-5xl opacity-30">chat</span><p class="text-sm font-medium">No messages yet. Start the conversation!</p></div>'; |
|
|
} |
|
|
} |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
console.error("Failed to fetch messages:", e); |
|
|
console.error("Failed to fetch messages:", e); |
|
|
} finally { |
|
|
} finally { |
|
|
isFetchingMessages = false; // 🟢 باز کردن قفل در هر شرایطی (چه موفق چه ارور)
|
|
|
|
|
|
|
|
|
isFetchingMessages = false; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// رندر کردن حبابهای پیام
|
|
|
|
|
|
function renderMessages(messages) { |
|
|
function renderMessages(messages) { |
|
|
const html = messages.map(m => { |
|
|
const html = messages.map(m => { |
|
|
const isMe = m.is_me; |
|
|
const isMe = m.is_me; |
|
|
@ -148,15 +242,10 @@ document.addEventListener('DOMContentLoaded', function () { |
|
|
UI.messages.insertAdjacentHTML('beforeend', html); |
|
|
UI.messages.insertAdjacentHTML('beforeend', html); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// اسکرول به پایین
|
|
|
|
|
|
function scrollToBottom() { |
|
|
function scrollToBottom() { |
|
|
UI.messages.scrollTo({ |
|
|
|
|
|
top: UI.messages.scrollHeight, |
|
|
|
|
|
behavior: 'smooth' |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
UI.messages.scrollTo({ top: UI.messages.scrollHeight, behavior: 'smooth' }); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// ارسال پیام جدید
|
|
|
|
|
|
UI.form.addEventListener('submit', async (e) => { |
|
|
UI.form.addEventListener('submit', async (e) => { |
|
|
e.preventDefault(); |
|
|
e.preventDefault(); |
|
|
const text = UI.input.value.trim(); |
|
|
const text = UI.input.value.trim(); |
|
|
@ -173,15 +262,11 @@ document.addEventListener('DOMContentLoaded', function () { |
|
|
}, |
|
|
}, |
|
|
body: JSON.stringify({ content: text }) |
|
|
body: JSON.stringify({ content: text }) |
|
|
}); |
|
|
}); |
|
|
// 🟢 صدا زدن دریافت پیام به صورت ایمن
|
|
|
|
|
|
await fetchMessages(); |
|
|
await fetchMessages(); |
|
|
fetchRooms(); |
|
|
fetchRooms(); |
|
|
} catch (e) { |
|
|
|
|
|
console.error("Failed to send message:", e); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
} catch (e) { console.error("Failed to send message:", e); } |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
// راهاندازی Polling
|
|
|
|
|
|
function startPolling() { |
|
|
function startPolling() { |
|
|
messagePollInterval = setInterval(fetchMessages, 3000); |
|
|
messagePollInterval = setInterval(fetchMessages, 3000); |
|
|
} |
|
|
} |
|
|
@ -190,8 +275,6 @@ document.addEventListener('DOMContentLoaded', function () { |
|
|
if (messagePollInterval) clearInterval(messagePollInterval); |
|
|
if (messagePollInterval) clearInterval(messagePollInterval); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
// مقداردهی اولیه
|
|
|
|
|
|
fetchRooms(); |
|
|
fetchRooms(); |
|
|
roomPollInterval = setInterval(fetchRooms, 10000); |
|
|
roomPollInterval = setInterval(fetchRooms, 10000); |
|
|
|
|
|
|
|
|
}); |
|
|
}); |