Browse Source

chat added for admin panel

building a new view for chat page of admin panel , handling getting messages and send messages

create html , css and js files for style and design of chat page

add a hook to fastapi to update chat realtimely
master
Mohsen Taba 2 months ago
parent
commit
ca92cf2797
  1. 149
      apps/chat/admin_views.py
  2. 12
      config/settings/base.py
  3. 295
      static/css/live_chat.css
  4. 197
      static/js/live_chat.js
  5. 94
      templates/admin/chat/live_chat.html
  6. 9
      utils/admin.py

149
apps/chat/admin_views.py

@ -0,0 +1,149 @@
import json
import requests
import threading # 🟢 برای اجرای بک‌گراند
from django.conf import settings
from django.http import JsonResponse
from django.shortcuts import render, get_object_or_404
from django.views.decorators.http import require_http_methods
from django.db.models import Q
from django.utils.translation import gettext_lazy as _
from apps.chat.models import RoomMessage, ChatMessage
from utils.admin import project_admin_site
def send_webhook_to_fastapi(room_id, message_data):
fastapi_url = f"{getattr(settings, 'FASTAPI_SERVER_URL', 'http://127.0.0.1:8000')}/api/internal/webhook/chat-message/"
headers = {
"Authorization": getattr(settings, 'INTERNAL_WEBHOOK_SECRET', 'super-secret-key-12345'),
"Content-Type": "application/json"
}
payload = {
"room_id": room_id,
"message": message_data
}
try:
# با تایم‌اوت 2 ثانیه می‌فرستیم که اگر سرور گیر بود، پروسه متوقف نشود
requests.post(fastapi_url, json=payload, headers=headers, timeout=2.0)
except Exception as e:
print(f"⚠️ Webhook to FastAPI failed: {e}")
def live_chat_dashboard_view(request):
"""رندر کردن اسکلت اصلی صفحه چت"""
context = {
**project_admin_site.each_context(request),
"title": _("Live Support Chat"),
}
return render(request, "admin/chat/live_chat.html", context)
def api_get_rooms(request):
"""گرفتن لیست روم‌ها بر اساس دسترسی ادمین"""
user = request.user
# ادمین کل همه روم‌ها رو میبینه، استاد فقط روم‌های خودش رو
if user.is_superuser or user.has_role('admin'):
rooms = RoomMessage.objects.all().order_by('-updated_at')[:50]
else:
rooms = RoomMessage.objects.filter(
Q(initiator=user) | Q(recipient=user) | Q(course__professor=user)
).distinct().order_by('-updated_at')[:50]
data = []
for r in rooms:
title = r.name or f"Room #{r.id}"
if r.room_type == 'group' and r.course:
title = r.course.title
data.append({
"id": r.id,
"title": title,
"type": r.room_type,
"unread": r.unread_messages_count,
"time": r.updated_at.strftime("%H:%M") if r.updated_at else ""
})
return JsonResponse({"rooms": data})
def api_get_messages(request, room_id):
"""گرفتن پیام‌های یک روم خاص (فقط پیام‌های جدیدتر از last_id)"""
last_id = int(request.GET.get('last_id', 0))
room = get_object_or_404(RoomMessage, id=room_id)
messages = ChatMessage.objects.filter(
room=room,
id__gt=last_id, # جادوی پولینگ: فقط آیدی‌های بزرگتر از آخرین پیامی که داریم رو بگیر
is_deleted=False
).order_by('id')
data = []
for m in messages:
avatar = m.sender.avatar.url if getattr(m.sender, 'avatar', None) else None
sender_name = m.sender.fullname or m.sender.email or "Unknown"
data.append({
"id": m.id,
"sender_id": m.sender_id,
"sender_name": sender_name,
"avatar": avatar,
"content": m.content,
"type": m.content_type,
"file_url": m.file_url, # از پراپرتی مدلت استفاده کردیم
"sent_at": m.sent_at.strftime("%H:%M"),
"is_me": m.sender_id == request.user.id
})
return JsonResponse({"messages": data})
@require_http_methods(["POST"])
def api_send_message(request, room_id):
"""ارسال پیام جدید از طریق ایجکس (AJAX)"""
try:
body = json.loads(request.body)
content = body.get('content', '').strip()
if not content:
return JsonResponse({"error": "Empty content"}, status=400)
room = get_object_or_404(RoomMessage, id=room_id)
# ذخیره در دیتابیس
msg = ChatMessage.objects.create(
room=room,
sender=request.user,
content=content,
content_type='text'
)
# آپدیت کردن زمان روم
room.updated_at = msg.sent_at
room.save(update_fields=['updated_at'])
# 🟢 ساخت ساختار JSON پیام دقیقاً شبیه به چیزی که FastAPI به فرانت می‌فرستد
avatar_url = request.user.avatar.url if getattr(request.user, 'avatar', None) else None
sender_name = request.user.fullname or request.user.email or "Support"
message_data = {
"id": msg.id,
"room_id": room_id,
"sender": {
"id": request.user.id,
"email": request.user.email,
"fullname": sender_name,
"user_type": getattr(request.user, 'user_type', 'admin'),
"avatar": avatar_url
},
"content": msg.content,
"content_type": msg.content_type,
"content_size": msg.content_size,
"sent_at": msg.sent_at.isoformat(),
"metadata": msg.message_metadata,
"updated_at": None,
"is_deleted": False
}
# 🟢 شلیک به سمت FastAPI در یک Thread جداگانه
threading.Thread(target=send_webhook_to_fastapi, args=(room_id, message_data)).start()
return JsonResponse({"success": True, "message_id": msg.id})
except Exception as e:
return JsonResponse({"error": str(e)}, status=500)

12
config/settings/base.py

@ -674,6 +674,12 @@ UNFOLD = {
"link": lambda request: admin_url_generator(request, "blog_blog_changelist"), "link": lambda request: admin_url_generator(request, "blog_blog_changelist"),
"permission": is_main_panel, "permission": is_main_panel,
}, },
{
"title": _("Live Chat"),
"icon": "forum", # آیکون چت
"link": lambda request: admin_url_generator(request, "live_chat_dashboard"),
"permission": is_main_panel,
},
] ]
}, },
# --- DOVOODI SECTIONS --- # --- DOVOODI SECTIONS ---
@ -902,4 +908,8 @@ PLAUSIBLE_DOMAIN = env("PLAUSIBLE_DOMAIN")
sentry_sdk.init( sentry_sdk.init(
dsn="https://31aaeeb3a42f9a8c1b26272a0cb8ad3e@o4507991743725568.ingest.us.sentry.io/4511127356768256", dsn="https://31aaeeb3a42f9a8c1b26272a0cb8ad3e@o4507991743725568.ingest.us.sentry.io/4511127356768256",
send_default_pii=True, send_default_pii=True,
)
)
INTERNAL_WEBHOOK_SECRET = env('INTERNAL_WEBHOOK_SECRET', default='super-secret-key-12345')
FASTAPI_SERVER_URL = env('FASTAPI_SERVER_URL', default='http://88.99.212.243:8020')

295
static/css/live_chat.css

@ -0,0 +1,295 @@
/* ── در هم شکستن محدودیت عرض و پدینگ‌های پیش‌فرض انفولد ── */
#content {
padding-left: 0 !important;
padding-right: 0 !important;
padding-bottom: 0 !important;
max-width: 100% !important;
width: 100% !important;
}
div:has(> #content) {
padding-left: 0 !important;
padding-right: 0 !important;
padding-bottom: 0 !important;
}
body,
html {
overflow: hidden !important;
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, sans-serif !important;
}
/* ── Layout & Structure ── */
.chat-main-wrapper {
height: calc(100vh - 140px);
margin-top: -1rem;
}
.rooms-sidebar {
width: 320px;
background: #f8fafc;
/* رنگ پس‌زمینه خیلی ملایم برای سایدبار */
}
@media (min-width: 1024px) {
.rooms-sidebar {
width: 360px;
}
}
.dark .rooms-sidebar {
background: #1f2937;
}
/* ── Custom Scrollbar ── */
.chat-scrollbar::-webkit-scrollbar,
.chat-messages::-webkit-scrollbar {
width: 6px;
}
.chat-scrollbar::-webkit-scrollbar-track,
.chat-messages::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.chat-scrollbar::-webkit-scrollbar-thumb,
.chat-messages::-webkit-scrollbar-thumb {
background: rgba(0, 136, 204, 0.3);
border-radius: 4px;
transition: all 0.3s ease;
}
.chat-scrollbar::-webkit-scrollbar-thumb:hover,
.chat-messages::-webkit-scrollbar-thumb:hover {
background: rgba(0, 136, 204, 0.5);
}
/* ── Room List Styles (New Telegram/Modern Style) ── */
.room-item {
margin: 4px 8px;
/* ایجاد فاصله از دیواره‌های سایدبار */
border-radius: 12px;
/* گرد کردن گوشه‌های کل روم */
border-left: none !important;
/* حذف بوردر چپ قدیمی */
transition: background-color 0.2s ease, transform 0.1s ease;
}
.room-item:hover {
background-color: rgba(0, 136, 204, 0.05);
}
.dark .room-item:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.room-item-active {
background: linear-gradient(135deg, rgba(0, 136, 204, 0.1), rgba(28, 156, 232, 0.05)) !important;
box-shadow: 0 2px 8px rgba(0, 136, 204, 0.05);
}
.dark .room-item-active {
background: linear-gradient(135deg, rgba(28, 156, 232, 0.15), rgba(0, 136, 204, 0.05)) !important;
}
/* آواتار کوچک داخل لیست چت‌ها */
.room-avatar {
width: 44px;
height: 44px;
border-radius: 50%;
background: linear-gradient(135deg, #cbd5e1, #94a3b8);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
flex-shrink: 0;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
}
.room-item-active .room-avatar {
background: linear-gradient(135deg, #0088cc, #1c9ce8);
}
/* ── Telegram-style Header ── */
.telegram-header {
background: linear-gradient(135deg, #0088cc, #1c9ce8);
border: none;
box-shadow: 0 2px 8px rgba(0, 136, 204, 0.2);
backdrop-filter: blur(20px);
}
.dark .telegram-header {
background: linear-gradient(135deg, #1f2937, #374151);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 11px;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid rgba(255, 255, 255, 0.25);
}
/* ── Chat Area Background ── */
.chat-messages {
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.5), rgba(248, 250, 252, 0.8));
overflow-y: auto;
overscroll-behavior: contain;
padding: 1.5rem;
}
.dark .chat-messages {
background: linear-gradient(to bottom, rgba(17, 24, 39, 0.5), rgba(31, 41, 55, 0.8));
}
/* ── Message Bubbles (Telegram Style) ── */
.message-bubble {
max-width: 80%;
margin-bottom: 16px;
animation: slideInUp 0.3s cubic-bezier(0.165, 0.84, 0.44, 1);
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(15px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.message-sent {
margin-left: auto;
margin-right: 0;
display: flex;
justify-content: flex-end;
}
.message-received {
margin-right: auto;
margin-left: 0;
display: flex;
justify-content: flex-start;
}
.message-bubble-sent {
background: linear-gradient(135deg, #0088cc, #1c9ce8);
color: white;
border-radius: 18px 18px 4px 18px;
box-shadow: 0 2px 8px rgba(0, 136, 204, 0.25);
}
.message-bubble-received {
background: #ffffff;
color: #374151;
border-radius: 18px 18px 18px 4px;
border: 1px solid rgba(229, 231, 235, 0.6);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.dark .message-bubble-received {
background: #374151;
color: #f9fafb;
border: 1px solid rgba(75, 85, 99, 0.3);
}
.message-content {
padding: 10px 14px;
font-size: 14px;
line-height: 1.6;
word-wrap: break-word;
}
.message-meta {
font-size: 10px;
opacity: 0.7;
margin-top: 4px;
text-align: right;
font-weight: 500;
}
/* ── Input Area ── */
.message-input-area {
background: linear-gradient(135deg, #ffffff, #f8fafc);
border-top: 1px solid rgba(229, 231, 235, 0.3);
}
.dark .message-input-area {
background: linear-gradient(135deg, #374151, #4b5563);
border-top: 1px solid rgba(75, 85, 99, 0.3);
}
.telegram-input-container {
background: #ffffff;
border: 2px solid rgba(0, 136, 204, 0.1);
border-radius: 25px;
padding: 6px 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);
transition: all 0.3s ease;
}
.telegram-input-container:focus-within {
border-color: #0088cc;
box-shadow: 0 6px 25px rgba(0, 136, 204, 0.12);
}
.dark .telegram-input-container {
background: rgba(55, 65, 81, 0.9);
border-color: rgba(75, 85, 99, 0.3);
}
.dark .telegram-input-container:focus-within {
border-color: #1c9ce8;
}
.telegram-message-input {
background: transparent;
border: none;
outline: none;
font-size: 14px;
padding: 8px 0;
color: #374151;
}
.dark .telegram-message-input {
color: #f9fafb;
}
.telegram-message-input::placeholder {
color: #9ca3af;
}
.telegram-action-btn {
width: 44px;
height: 44px;
border: none;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
cursor: pointer;
flex-shrink: 0;
}
.send-btn-telegram {
background: linear-gradient(135deg, #0088cc, #1c9ce8);
color: white;
}
.send-btn-telegram:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 136, 204, 0.4);
}
.send-btn-telegram:active {
transform: scale(0.95);
}

197
static/js/live_chat.js

@ -0,0 +1,197 @@
document.addEventListener('DOMContentLoaded', function () {
// متغیرهای وضعیت
let currentRoomId = null;
let lastMessageId = 0;
let messagePollInterval = null;
let roomPollInterval = null;
// 🟢 متغیر قفل برای جلوگیری از دریافت پیام‌های تکراری (Race Condition)
let isFetchingMessages = false;
// المان‌های DOM
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'),
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')
};
// گرفتن لیست چت‌ها از سرور
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, "\\'");
return `
<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>
</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>
</div>
`;
}).join('');
}
// انتخاب یک چت
window.selectRoom = function (id, title) {
currentRoomId = id;
lastMessageId = 0;
isFetchingMessages = false; // ریست کردن قفل
UI.noChatSelected.classList.add('hidden');
UI.chatHeader.classList.remove('invisible');
UI.messages.classList.remove('invisible');
UI.inputArea.classList.remove('invisible');
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>';
fetchRooms();
stopPolling();
fetchMessages().then(() => {
startPolling();
scrollToBottom();
});
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 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>`;
}
} 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, "&lt;").replace(/>/g, "&gt;");
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'
});
}
// ارسال پیام جدید
UI.form.addEventListener('submit', async (e) => {
e.preventDefault();
const text = UI.input.value.trim();
if (!text || !currentRoomId) return;
UI.input.value = '';
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);
}
});
// راه‌اندازی Polling
function startPolling() {
messagePollInterval = setInterval(fetchMessages, 3000);
}
function stopPolling() {
if (messagePollInterval) clearInterval(messagePollInterval);
}
// مقداردهی اولیه
fetchRooms();
roomPollInterval = setInterval(fetchRooms, 10000);
});

94
templates/admin/chat/live_chat.html

@ -0,0 +1,94 @@
{% extends "admin/base_site.html" %}
{% load i18n static unfold %}
{% block breadcrumbs %}{% endblock %}
{% block title %}
{{ title }} | {{ site_title|default:_('Django site admin') }}
{% endblock %}
{% block extrahead %}
{{ block.super }}
<link href="https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/Vazirmatn-font-face.css" rel="stylesheet"
type="text/css" />
<link rel="stylesheet" href="{% static 'css/live_chat.css' %}">
{% endblock %}
{% 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="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="flex items-center gap-3">
<div class="user-avatar bg-white/20 border border-white/30 text-white shadow-sm">
<span class="material-symbols-outlined text-[20px]">forum</span>
</div>
<h2 class="text-[16px] font-semibold text-white m-0 tracking-wide">
{% trans "Chat Rooms" %}
</h2>
</div>
</div>
<div id="room-list" class="flex-1 overflow-y-auto overscroll-contain chat-scrollbar pt-2">
<div class="flex items-center justify-center h-full">
<span class="material-symbols-outlined animate-spin text-gray-400">autorenew</span>
</div>
</div>
</div>
<div class="chat-area flex-1 flex flex-col relative z-10">
<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]">
<div
class="w-20 h-20 mx-auto mb-6 bg-gradient-to-br from-blue-100 to-blue-200 dark:from-gray-700 dark:to-gray-600 rounded-full flex items-center justify-center shadow-inner">
<span class="material-symbols-outlined text-5xl text-blue-500 dark:text-blue-400 opacity-80">chat</span>
</div>
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-2">{% trans "No Chat Selected" %}</h3>
<p class="text-sm font-medium">{% trans "Select a chat from the sidebar to start messaging" %}</p>
</div>
<div id="active-chat-header"
class="telegram-header px-6 py-3 flex items-center justify-between flex-shrink-0 invisible">
<div class="flex items-center gap-3">
<div class="user-avatar bg-white/20 border border-white/30 text-white shadow-sm">
<span class="material-symbols-outlined text-[20px]">group</span>
</div>
<h3 id="active-chat-title" class="text-[16px] font-semibold text-white m-0 tracking-wide"></h3>
</div>
<span id="polling-indicator" class="flex h-2.5 w-2.5 relative hidden">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-300 opacity-75"></span>
<span
class="relative inline-flex rounded-full h-2.5 w-2.5 bg-green-400 shadow-[0_0_8px_rgba(74,222,128,0.8)]"></span>
</span>
</div>
<div id="chat-messages" class="chat-messages flex-1 invisible"></div>
<div id="chat-input-area" class="message-input-area flex-shrink-0 px-6 py-4 invisible z-20">
<form id="chat-form" class="flex items-center gap-3 w-full m-0 max-w-5xl mx-auto">
<div class="telegram-input-container flex-1">
<input type="text" id="chat-input" class="telegram-message-input w-full"
placeholder="{% trans 'Type your message...' %}" autocomplete="off">
</div>
<button type="submit" id="send-btn" class="telegram-action-btn send-btn-telegram shadow-md">
<span class="material-symbols-outlined text-[20px] ml-1">send</span>
</button>
</form>
</div>
</div>
</div>
<script>
window.CHAT_CONFIG = {
csrfToken: '{{ csrf_token }}',
apiUrlRooms: '{% url "admin:live_chat_api_rooms" %}',
apiUrlMessagesBase: '{% url "admin:live_chat_api_messages" room_id=0 %}'.replace('0/', ''),
apiUrlSendBase: '{% url "admin:live_chat_api_send" room_id=0 %}'.replace('0/', '')
};
</script>
<script src="{% static 'js/live_chat.js' %}"></script>
{% endblock %}

9
utils/admin.py

@ -156,8 +156,17 @@ 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
custom_urls = [ custom_urls = [
path('toggle_sidebar/', toggle_sidebar, name='toggle_sidebar'), path('toggle_sidebar/', toggle_sidebar, name='toggle_sidebar'),
# روت اصلی صفحه:
path('live-chat/', self.admin_view(live_chat_dashboard_view), name='live_chat_dashboard'),
# روت‌های API برای Polling جاوااسکریپت:
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/send/<int:room_id>/', self.admin_view(api_send_message), name='live_chat_api_send'),
] ]
return custom_urls + urls return custom_urls + urls

Loading…
Cancel
Save