Browse Source

admin can search users and create a new chat room on the admin panel

master
Mohsen Taba 2 months ago
parent
commit
2705527934
  1. 28
      static/css/live_chat.css
  2. 169
      static/js/live_chat.js
  3. 47
      templates/admin/chat/live_chat.html
  4. 4
      utils/admin.py

28
static/css/live_chat.css

@ -293,3 +293,31 @@ html {
.send-btn-telegram:active { .send-btn-telegram:active {
transform: scale(0.95); transform: scale(0.95);
} }
/* ── Users Modal Solid Backgrounds ── */
#users-modal {
background-color: #f8fafc !important;
}
#users-modal > div:nth-child(2) {
background-color: #ffffff !important;
border-bottom: 1px solid #e5e7eb !important;
}
#users-list {
background-color: #f8fafc !important;
}
/* Dark Mode Support */
.dark #users-modal {
background-color: #1f2937 !important;
}
.dark #users-modal > div:nth-child(2) {
background-color: #111827 !important;
border-bottom: 1px solid #374151 !important;
}
.dark #users-list {
background-color: #1f2937 !important;
}

169
static/js/live_chat.js

@ -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);
}); });

47
templates/admin/chat/live_chat.html

@ -15,12 +15,13 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="chat-main-wrapper flex w-full shadow-md border-y border-gray-200 dark:border-gray-700 overflow-hidden mt-0">
<div
class="chat-main-wrapper flex w-full shadow-md border-y border-gray-200 dark:border-gray-700 overflow-hidden mt-0 relative">
<div <div
class="rooms-sidebar flex-shrink-0 flex flex-col border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 relative z-20"> class="rooms-sidebar flex-shrink-0 flex flex-col border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 relative z-20">
<div class="telegram-header px-5 py-3 flex-shrink-0 flex items-center shadow-sm">
<div class="telegram-header px-5 py-3 flex-shrink-0 flex items-center justify-between shadow-sm">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="user-avatar bg-white/20 border border-white/30 text-white shadow-sm"> <div class="user-avatar bg-white/20 border border-white/30 text-white shadow-sm">
<span class="material-symbols-outlined text-[20px]">forum</span> <span class="material-symbols-outlined text-[20px]">forum</span>
@ -29,17 +30,50 @@
{% trans "Chat Rooms" %} {% trans "Chat Rooms" %}
</h2> </h2>
</div> </div>
<button type="button" id="new-chat-btn"
class="w-8 h-8 rounded-full bg-white/20 hover:bg-white/30 border border-white/30 text-white flex items-center justify-center transition-colors shadow-sm"
title="{% trans 'New Chat' %}">
<span class="material-symbols-outlined text-[20px]">edit_square</span>
</button>
</div> </div>
<div id="room-list" class="flex-1 overflow-y-auto overscroll-contain chat-scrollbar pt-2">
<div id="room-list" class="flex-1 overflow-y-auto overscroll-contain chat-scrollbar pt-2 relative">
<div class="flex items-center justify-center h-full"> <div class="flex items-center justify-center h-full">
<span class="material-symbols-outlined animate-spin text-gray-400">autorenew</span> <span class="material-symbols-outlined animate-spin text-gray-400">autorenew</span>
</div> </div>
</div> </div>
<div id="users-modal"
class="absolute inset-0 z-50 bg-gray-50 dark:bg-gray-900 flex flex-col transform transition-transform duration-300 translate-y-full shadow-2xl">
<div class="telegram-header px-5 py-3 flex items-center gap-3 shadow-sm">
<button type="button" id="close-users-modal"
class="text-white hover:bg-white/20 p-1.5 rounded-full transition-colors flex items-center justify-center">
<span class="material-symbols-outlined text-[22px]">arrow_back</span>
</button>
<h3 class="text-[16px] font-semibold text-white m-0 tracking-wide">{% trans "Select Contact" %}</h3>
</div>
<div class="p-4 border-b border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-800 shadow-sm">
<div class="relative telegram-input-container !p-1.5">
<span
class="material-symbols-outlined absolute left-4 top-1/2 -translate-y-1/2 text-gray-400 text-[20px]">search</span>
<input type="text" id="user-search-input"
class="telegram-message-input w-full pl-11 pr-4 py-1 !min-h-[35px]"
placeholder="{% trans 'Search users...' %}">
</div>
</div>
<div id="users-list" class="flex-1 overflow-y-auto overscroll-contain chat-scrollbar pt-2 pb-4">
<div class="flex items-center justify-center h-20">
<span class="material-symbols-outlined animate-spin text-gray-400">autorenew</span>
</div>
</div>
</div>
</div> </div>
<div class="chat-area flex-1 flex flex-col relative z-10"> <div class="chat-area flex-1 flex flex-col relative z-10">
<div id="no-chat-selected" <div id="no-chat-selected"
class="absolute inset-0 flex flex-col items-center justify-center text-gray-400 z-30 bg-[#f8fafc] dark:bg-[#111827]"> class="absolute inset-0 flex flex-col items-center justify-center text-gray-400 z-30 bg-[#f8fafc] dark:bg-[#111827]">
<div <div
@ -83,11 +117,14 @@
</div> </div>
<script> <script>
// 🟢 اضافه شدن آدرس‌های API جدید به کانفیگ
window.CHAT_CONFIG = { window.CHAT_CONFIG = {
csrfToken: '{{ csrf_token }}', csrfToken: '{{ csrf_token }}',
apiUrlRooms: '{% url "admin:live_chat_api_rooms" %}', apiUrlRooms: '{% url "admin:live_chat_api_rooms" %}',
apiUrlMessagesBase: '{% url "admin:live_chat_api_messages" room_id=0 %}'.replace('0/', ''), apiUrlMessagesBase: '{% url "admin:live_chat_api_messages" room_id=0 %}'.replace('0/', ''),
apiUrlSendBase: '{% url "admin:live_chat_api_send" room_id=0 %}'.replace('0/', '')
apiUrlSendBase: '{% url "admin:live_chat_api_send" room_id=0 %}'.replace('0/', ''),
apiUrlUsers: '{% url "admin:live_chat_api_users" %}', // روت جدید برای گرفتن کاربران
apiUrlCreateRoom: '{% url "admin:live_chat_api_create_room" %}' // روت جدید برای ساخت روم
}; };
</script> </script>
<script src="{% static 'js/live_chat.js' %}"></script> <script src="{% static 'js/live_chat.js' %}"></script>

4
utils/admin.py

@ -156,7 +156,7 @@ class FormulaAdminSite(UnfoldAdminSite):
def get_urls(self): def get_urls(self):
urls = super().get_urls() urls = super().get_urls()
from apps.chat.admin_views import live_chat_dashboard_view, api_get_rooms, api_get_messages, api_send_message
from apps.chat.admin_views import live_chat_dashboard_view, api_get_rooms, api_get_messages, api_send_message, api_create_or_get_room , api_get_users
custom_urls = [ custom_urls = [
path('toggle_sidebar/', toggle_sidebar, name='toggle_sidebar'), path('toggle_sidebar/', toggle_sidebar, name='toggle_sidebar'),
@ -167,6 +167,8 @@ class FormulaAdminSite(UnfoldAdminSite):
path('live-chat/api/rooms/', self.admin_view(api_get_rooms), name='live_chat_api_rooms'), path('live-chat/api/rooms/', self.admin_view(api_get_rooms), name='live_chat_api_rooms'),
path('live-chat/api/messages/<int:room_id>/', self.admin_view(api_get_messages), name='live_chat_api_messages'), path('live-chat/api/messages/<int:room_id>/', self.admin_view(api_get_messages), name='live_chat_api_messages'),
path('live-chat/api/send/<int:room_id>/', self.admin_view(api_send_message), name='live_chat_api_send'), path('live-chat/api/send/<int:room_id>/', self.admin_view(api_send_message), name='live_chat_api_send'),
path('live-chat/api/create-or-get-room/', self.admin_view(api_create_or_get_room), name='live_chat_api_create_room'),
path('live-chat/api/get-users/', self.admin_view(api_get_users), name='live_chat_api_users'),
] ]
return custom_urls + urls return custom_urls + urls

Loading…
Cancel
Save