You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
305 lines
13 KiB
305 lines
13 KiB
document.addEventListener('DOMContentLoaded', function () {
|
|
|
|
let currentRoomId = null;
|
|
let lastMessageId = 0;
|
|
let messagePollInterval = null;
|
|
let roomPollInterval = null;
|
|
let isFetchingMessages = false;
|
|
|
|
const UI = {
|
|
roomList: document.getElementById('room-list'),
|
|
noChatSelected: document.getElementById('no-chat-selected'),
|
|
chatHeader: document.getElementById('active-chat-header'),
|
|
chatTitle: document.getElementById('active-chat-title'),
|
|
chatHeaderInfo: document.getElementById('chat-header-info'),
|
|
messages: document.getElementById('chat-messages'),
|
|
inputArea: document.getElementById('chat-input-area'),
|
|
form: document.getElementById('chat-form'),
|
|
input: document.getElementById('chat-input'),
|
|
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, #0A522E, #052B18); box-shadow: 0 2px 8px rgba(10, 82, 46, 0.3); color: #FAF6E9;">
|
|
${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() {
|
|
try {
|
|
const res = await fetch(window.CHAT_CONFIG.apiUrlRooms);
|
|
const data = await res.json();
|
|
renderRooms(data.rooms);
|
|
} catch (e) { console.error("Failed to fetch rooms:", e); }
|
|
}
|
|
|
|
function renderRooms(rooms) {
|
|
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>';
|
|
return;
|
|
}
|
|
|
|
UI.roomList.innerHTML = rooms.map(room => {
|
|
const isActive = currentRoomId === room.id;
|
|
const safeTitle = room.title.replace(/'/g, "\\'");
|
|
const initial = room.title ? room.title.charAt(0).toUpperCase() : 'G';
|
|
|
|
return `
|
|
<div onclick="selectRoom(${room.id}, '${safeTitle}')"
|
|
class="room-item flex items-center gap-3 p-3 cursor-pointer ${isActive ? 'room-item-active' : ''}">
|
|
|
|
<div class="room-avatar">
|
|
${initial}
|
|
</div>
|
|
|
|
<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="shadow-sm text-[10px] font-bold rounded-full unread-badge">${room.unread}</span>` : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
window.selectRoom = function (id, title) {
|
|
currentRoomId = id;
|
|
lastMessageId = 0;
|
|
isFetchingMessages = false;
|
|
|
|
// 🟢 Optimistic UI: Update active state and remove unread badge immediately
|
|
document.querySelectorAll('.room-item').forEach(el => el.classList.remove('room-item-active'));
|
|
const clickedRoom = document.querySelector(`.room-item[onclick*="selectRoom(${id}"]`);
|
|
if (clickedRoom) {
|
|
clickedRoom.classList.add('room-item-active');
|
|
const badge = clickedRoom.querySelector('.unread-badge');
|
|
if (badge) badge.remove(); // Hide badge instantly
|
|
}
|
|
|
|
// 🟢 تغییر invisible به hidden برای کارکرد درست بلوکها
|
|
UI.noChatSelected.classList.add('hidden');
|
|
UI.chatHeaderInfo.classList.remove('hidden'); // 🟢 تغییر یافته
|
|
UI.messages.classList.remove('hidden');
|
|
UI.inputArea.classList.remove('hidden');
|
|
|
|
UI.chatTitle.textContent = title;
|
|
UI.messages.innerHTML = '<div class="flex items-center justify-center h-full"><span class="material-symbols-outlined animate-spin text-gray-400 text-3xl">autorenew</span></div>';
|
|
|
|
stopPolling();
|
|
fetchMessages().then(() => {
|
|
startPolling();
|
|
scrollToBottom();
|
|
fetchRooms(); // Sync with backend to ensure exact unread counts
|
|
});
|
|
|
|
UI.input.focus();
|
|
};
|
|
|
|
async function fetchMessages() {
|
|
if (!currentRoomId || isFetchingMessages) return;
|
|
isFetchingMessages = true;
|
|
|
|
try {
|
|
UI.indicator.classList.remove('hidden');
|
|
setTimeout(() => UI.indicator.classList.add('hidden'), 500);
|
|
|
|
const res = await fetch(`${window.CHAT_CONFIG.apiUrlMessagesBase}${currentRoomId}/?last_id=${lastMessageId}`);
|
|
const data = await res.json();
|
|
|
|
if (data.messages && data.messages.length > 0) {
|
|
if (lastMessageId === 0) UI.messages.innerHTML = '';
|
|
|
|
renderMessages(data.messages);
|
|
lastMessageId = data.messages[data.messages.length - 1].id;
|
|
scrollToBottom();
|
|
} else if (lastMessageId === 0) {
|
|
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) {
|
|
console.error("Failed to fetch messages:", e);
|
|
} finally {
|
|
isFetchingMessages = false;
|
|
}
|
|
}
|
|
|
|
function renderMessages(messages) {
|
|
const html = messages.map(m => {
|
|
const isMe = m.is_me;
|
|
const alignClass = isMe ? 'message-sent' : 'message-received';
|
|
const bubbleClass = isMe ? 'message-bubble-sent' : 'message-bubble-received';
|
|
|
|
const senderName = !isMe ? `<div class="text-[11px] font-bold text-blue-600 dark:text-blue-400 mb-1 opacity-90">${m.sender_name}</div>` : '';
|
|
const safeContent = m.content.replace(/</g, "<").replace(/>/g, ">");
|
|
const timeColor = isMe ? 'text-blue-100' : 'text-gray-400';
|
|
const tickIcon = isMe ? `<span class="material-symbols-outlined text-[13px] ml-1 text-white opacity-80">done_all</span>` : '';
|
|
|
|
return `
|
|
<div class="message-bubble ${alignClass} w-full">
|
|
<div class="${bubbleClass}">
|
|
<div class="message-content">
|
|
${senderName}
|
|
<p class="m-0">${safeContent}</p>
|
|
<div class="message-meta ${timeColor} flex items-center justify-end">
|
|
${m.sent_at} ${tickIcon}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
UI.messages.insertAdjacentHTML('beforeend', html);
|
|
}
|
|
|
|
function scrollToBottom() {
|
|
UI.messages.scrollTo({ top: UI.messages.scrollHeight, behavior: 'smooth' });
|
|
}
|
|
|
|
// ── Textarea Auto-resize & Key Handling ──
|
|
UI.input.addEventListener('input', function () {
|
|
this.style.height = 'auto';
|
|
this.style.height = (this.scrollHeight) + 'px';
|
|
});
|
|
|
|
UI.input.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
// Trigger form submit
|
|
UI.form.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
|
|
}
|
|
});
|
|
|
|
UI.form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const text = UI.input.value.trim();
|
|
if (!text || !currentRoomId) return;
|
|
|
|
UI.input.value = '';
|
|
UI.input.style.height = 'auto'; // Reset height
|
|
|
|
try {
|
|
await fetch(`${window.CHAT_CONFIG.apiUrlSendBase}${currentRoomId}/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': window.CHAT_CONFIG.csrfToken
|
|
},
|
|
body: JSON.stringify({ content: text })
|
|
});
|
|
await fetchMessages();
|
|
fetchRooms();
|
|
} catch (e) { console.error("Failed to send message:", e); }
|
|
});
|
|
|
|
function startPolling() {
|
|
messagePollInterval = setInterval(fetchMessages, 3000);
|
|
}
|
|
|
|
function stopPolling() {
|
|
if (messagePollInterval) clearInterval(messagePollInterval);
|
|
}
|
|
|
|
fetchRooms();
|
|
roomPollInterval = setInterval(fetchRooms, 10000);
|
|
});
|