Browse Source
chat added for admin panel
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 realtimelymaster
6 changed files with 755 additions and 1 deletions
-
149apps/chat/admin_views.py
-
12config/settings/base.py
-
295static/css/live_chat.css
-
197static/js/live_chat.js
-
94templates/admin/chat/live_chat.html
-
9utils/admin.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) |
||||
@ -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); |
||||
|
} |
||||
@ -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, "<").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' |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// ارسال پیام جدید
|
||||
|
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); |
||||
|
|
||||
|
}); |
||||
@ -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 %} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue