572 lines
26 KiB
HTML
572 lines
26 KiB
HTML
{% extends 'base.html' %}
|
|
{% block content %}
|
|
<style>
|
|
.messenger-shell { background:#ebedf0; border-radius:24px; overflow:hidden; min-height:74vh; box-shadow:0 20px 50px rgba(47,63,87,.10); position:relative; }
|
|
.messenger-sidebar { border-right:1px solid #dadee0; background:#ebedf0; }
|
|
.messenger-conv-toolbar { background:#ebedf0; padding:22px; border-bottom:1px solid #dadee0; }
|
|
.messenger-chat-area { background:#fff; }
|
|
.messenger-search-input { border:none; border-radius:14px; background:#fff; padding:12px 14px; box-shadow: inset 0 0 0 1px #dbe0e5; }
|
|
.messenger-search-input:focus { outline:none; box-shadow:0 0 0 3px rgba(28,161,193,.14), inset 0 0 0 1px #1ca1c1; }
|
|
.convList { max-height: calc(74vh - 124px); overflow:auto; }
|
|
.convItem { display:block; padding:0; border:none; border-bottom:1px solid #dadee0; text-decoration:none; color:inherit; background:transparent; transition:all .18s ease; }
|
|
.convItem:hover { background:#f4f5f9; color:inherit; }
|
|
.convItem.active { background:#fff; box-shadow: inset 3px 0 #1ca1c1; }
|
|
.convOverall { display:flex; align-items:center; padding:14px 12px; gap:14px; }
|
|
.convImg, .chatAvatarCircle, .memberAvatarCircle { flex-shrink:0; display:flex; align-items:center; justify-content:center; border-radius:50%; color:#fff; font-weight:700; letter-spacing:.02em; }
|
|
.convImg { width:48px; height:48px; background:linear-gradient(135deg,#1ca1c1,#5875ff); font-size:.92rem; }
|
|
.convImg.group { background:linear-gradient(135deg,#6d5dfc,#1ca1c1); }
|
|
.convContent { min-width:0; flex:1; }
|
|
.convSubrow { display:flex; align-items:baseline; gap:10px; }
|
|
.convName { font-weight:600; max-width:220px; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
|
|
.convDate { margin-left:auto; color:#94a1b3; font-size:.78rem; white-space:nowrap; }
|
|
.convMessage { color:#475466; max-width:210px; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; font-size:.92rem; }
|
|
.convCounter { margin-left:auto; background:#1ca1c1; color:#fff; min-width:22px; height:22px; padding:0 6px; border-radius:999px; display:flex; align-items:center; justify-content:center; font-size:.76rem; font-weight:600; }
|
|
.chatHeader { padding:18px 24px; border-bottom:1px solid #edf0f3; display:flex; align-items:center; gap:18px; }
|
|
.chatHeaderInfo { min-width:0; flex:1; }
|
|
.chatTitle { font-size:1.08rem; font-weight:700; color:#273142; }
|
|
.chatMeta { display:flex; align-items:center; gap:8px; flex-wrap:wrap; color:#7b8797; font-size:.86rem; }
|
|
.userListStack { display:flex; align-items:center; }
|
|
.memberAvatarCircle { width:32px; height:32px; font-size:.74rem; border:2px solid #fff; margin-left:-10px; background:linear-gradient(135deg,#94a1b3,#475466); }
|
|
.memberAvatarCircle:first-child { margin-left:0; }
|
|
.memberCounter { width:32px; height:32px; border-radius:50%; background:#94a1b3; color:#fff; display:flex; align-items:center; justify-content:center; font-size:.74rem; font-weight:700; margin-left:-10px; border:2px solid #fff; }
|
|
.chatToolbarBtn { width:40px; height:40px; border-radius:50%; border:none; background:#f4f5f9; color:#475466; }
|
|
.chatMessagesWrap { background:linear-gradient(180deg,#fff 0%, #f8fafc 100%); min-height:420px; max-height:calc(74vh - 230px); overflow:auto; padding:24px; }
|
|
.chatRow { display:flex; margin-bottom:16px; }
|
|
.chatRow.me { justify-content:flex-end; }
|
|
.chatBubbleWrap { max-width:78%; display:flex; gap:12px; align-items:flex-end; }
|
|
.chatRow.me .chatBubbleWrap { flex-direction:row-reverse; }
|
|
.chatAvatarCircle { width:40px; height:40px; font-size:.78rem; background:linear-gradient(135deg,#1ca1c1,#475466); }
|
|
.commentCard { flex:1; background:#f4f5f9; border-radius:0 18px 18px 18px; padding:12px 16px 14px; box-shadow:0 6px 18px rgba(77,94,112,.05); }
|
|
.chatRow.me .commentCard { background:#1ca1c1; color:#fff; border-radius:18px 0 18px 18px; }
|
|
.commentHeader { display:flex; align-items:center; gap:8px; font-size:.76rem; margin-bottom:4px; color:#7b8797; }
|
|
.chatRow.me .commentHeader { color:rgba(255,255,255,.82); }
|
|
.commentName { font-weight:700; }
|
|
.commentTime { margin-left:auto; }
|
|
.commentBody { white-space:pre-wrap; word-break:break-word; line-height:1.45; font-size:.94rem; }
|
|
.attachmentPill { display:inline-flex; align-items:center; gap:8px; margin-top:10px; border-radius:999px; padding:8px 12px; text-decoration:none; font-size:.84rem; background:#fff; color:#334155; box-shadow: inset 0 0 0 1px rgba(148,161,179,.35); }
|
|
.chatRow.me .attachmentPill { background:rgba(255,255,255,.18); color:#fff; box-shadow: inset 0 0 0 1px rgba(255,255,255,.24); }
|
|
.chatComposer { border-top:1px solid #edf0f3; padding:18px 24px; background:#fff; }
|
|
.composerBox { border-radius:22px; background:#f4f5f9; padding:14px; box-shadow: inset 0 0 0 1px #dde3ea; }
|
|
.composerBox textarea { resize:none; border:none; background:transparent; box-shadow:none !important; min-height:54px; }
|
|
.composerBottom { display:flex; gap:12px; align-items:center; justify-content:space-between; flex-wrap:wrap; }
|
|
.composerFile { display:flex; align-items:center; gap:8px; color:#64748b; font-size:.85rem; }
|
|
.composerFile input { max-width:280px; }
|
|
.empty-chat-state { display:flex; align-items:center; justify-content:center; flex-direction:column; text-align:center; min-height:540px; padding:40px 22px; color:#64748b; }
|
|
.empty-chat-state .bigIcon { width:92px; height:92px; border-radius:50%; display:flex; align-items:center; justify-content:center; background:#f4f5f9; font-size:2.4rem; color:#1ca1c1; margin-bottom:18px; }
|
|
.sending-state { opacity:.6; pointer-events:none; }
|
|
|
|
.chat-mobile-toggle,
|
|
.chat-mobile-close {
|
|
width:44px;
|
|
height:44px;
|
|
border:0;
|
|
border-radius:14px;
|
|
background:#1ca1c1;
|
|
color:#fff;
|
|
display:inline-flex;
|
|
align-items:center;
|
|
justify-content:center;
|
|
box-shadow:0 10px 24px rgba(28,161,193,.24);
|
|
flex-shrink:0;
|
|
}
|
|
|
|
.chat-mobile-toggle i,
|
|
.chat-mobile-close i {
|
|
font-size:1.2rem;
|
|
line-height:1;
|
|
}
|
|
|
|
.chat-mobile-backdrop {
|
|
position:fixed;
|
|
inset:0;
|
|
background:rgba(15,23,42,.42);
|
|
opacity:0;
|
|
pointer-events:none;
|
|
transition:opacity .28s ease;
|
|
z-index:1040;
|
|
}
|
|
|
|
@media (max-width: 991.98px) {
|
|
.messenger-shell { border-radius:18px; overflow:visible; }
|
|
.chatHeader, .chatComposer, .chatMessagesWrap { padding-left:16px; padding-right:16px; }
|
|
.chatBubbleWrap { max-width:100%; }
|
|
.convList { max-height:none; }
|
|
|
|
.messenger-sidebar {
|
|
position:fixed;
|
|
top:0;
|
|
right:0;
|
|
bottom:0;
|
|
left:auto;
|
|
width:min(380px, 88vw);
|
|
max-width:100%;
|
|
height:100vh;
|
|
z-index:1050;
|
|
border-right:0;
|
|
border-left:1px solid #dadee0;
|
|
box-shadow:-18px 0 40px rgba(15,23,42,.18);
|
|
transform:translateX(100%);
|
|
transition:transform .30s ease;
|
|
display:flex;
|
|
flex-direction:column;
|
|
}
|
|
|
|
.messenger-conv-toolbar {
|
|
position:sticky;
|
|
top:0;
|
|
z-index:2;
|
|
}
|
|
|
|
.convList {
|
|
flex:1 1 auto;
|
|
overflow:auto;
|
|
}
|
|
|
|
.messenger-chat-area {
|
|
min-height:72vh;
|
|
}
|
|
|
|
#messengerApp.chat-sidebar-open .messenger-sidebar {
|
|
transform:translateX(0);
|
|
}
|
|
|
|
#messengerApp.chat-sidebar-open .chat-mobile-backdrop {
|
|
opacity:1;
|
|
pointer-events:auto;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div class="page-toolbar">
|
|
<div>
|
|
<h1 class="h3 mb-1">Chat</h1>
|
|
<p class="text-muted mb-0">Mensajería privada, grupos, adjuntos y avisos en tiempo real. Cada conversación queda aislada por institución.</p>
|
|
</div>
|
|
|
|
<button type="button" class="chat-mobile-toggle d-lg-none" id="chatSidebarToggle" aria-label="Abrir conversaciones">
|
|
<i class="bi bi-list"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div
|
|
class="messenger-shell row g-0"
|
|
id="messengerApp"
|
|
data-current-user-id="{{ current_user.id }}"
|
|
data-active-conversation-id="{{ active_conversation.id if active_conversation else '' }}"
|
|
data-chat-base-url="{{ url_for('admin_chat') }}"
|
|
>
|
|
<div class="chat-mobile-backdrop" id="chatSidebarBackdrop"></div>
|
|
|
|
<aside class="col-lg-4 messenger-sidebar" id="chatSidebarPanel">
|
|
<div class="messenger-conv-toolbar">
|
|
<div class="d-flex justify-content-between align-items-center gap-2 mb-3">
|
|
<div>
|
|
<div class="fw-semibold" style="font-size:1.05rem; color:#273142;">Conversaciones ({{ conversations|length }})</div>
|
|
<div class="small text-muted">Privados y grupos de {{ current_institution.display_name if current_institution else 'la plataforma' }}</div>
|
|
</div>
|
|
|
|
<div class="d-flex align-items-center gap-2">
|
|
<button type="button" class="chat-mobile-close d-lg-none" id="chatSidebarClose" aria-label="Cerrar conversaciones">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
<button class="btn btn-primary btn-sm rounded-circle shadow-sm" style="width:42px;height:42px;" data-bs-toggle="modal" data-bs-target="#newChatModal" aria-label="Nueva conversación">
|
|
<i class="bi bi-plus-lg"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<input type="search" class="form-control messenger-search-input" id="conversationSearch" placeholder="Buscar conversación...">
|
|
</div>
|
|
|
|
<div class="convList" id="conversationList">
|
|
{% for item in conversations %}
|
|
{% set convo = item.conversation %}
|
|
{% set is_active = active_conversation and active_conversation.id == convo.id %}
|
|
{% set initials = (item.display_name or 'C').split() %}
|
|
<a
|
|
class="convItem {% if is_active %}active{% endif %}"
|
|
href="{{ url_for('admin_chat', conversation_id=convo.id) }}"
|
|
data-conversation-id="{{ convo.id }}"
|
|
data-search="{{ (item.display_name ~ ' ' ~ (item.last_message.body if item.last_message and item.last_message.body else ''))|lower }}"
|
|
>
|
|
<div class="convOverall">
|
|
<div class="convImg {% if convo.is_group %}group{% endif %}">{{ ((initials[0][0] if initials else 'C') ~ (initials[1][0] if initials|length > 1 else ''))|upper }}</div>
|
|
<div class="convContent">
|
|
<div class="convSubrow">
|
|
<span class="convName">{{ item.display_name }}</span>
|
|
<span class="convDate">{{ (item.last_message.created_at if item.last_message else convo.created_at).strftime('%H:%M') }}</span>
|
|
</div>
|
|
<div class="convSubrow">
|
|
<span class="convMessage">{{ item.last_message.body if item.last_message and item.last_message.body else ('Archivo adjunto' if item.last_message and item.last_message.attachments else 'Sin mensajes') }}</span>
|
|
<span class="convCounter {% if not item.unread %}d-none{% endif %}">{{ item.unread or 0 }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
{% else %}
|
|
<div class="p-4 text-muted text-center">Todavía no hay conversaciones.</div>
|
|
{% endfor %}
|
|
</div>
|
|
</aside>
|
|
|
|
<div class="col-lg-8 messenger-chat-area">
|
|
{% if active_conversation %}
|
|
<div class="chatHeader">
|
|
<button type="button" class="chat-mobile-toggle d-lg-none" id="chatSidebarToggleInline" aria-label="Abrir conversaciones">
|
|
<i class="bi bi-list"></i>
|
|
</button>
|
|
|
|
<div class="chatAvatarCircle">{{ (active_chat_title or 'C')[:2]|upper }}</div>
|
|
|
|
<div class="chatHeaderInfo">
|
|
<div class="chatTitle">{{ active_chat_title }}</div>
|
|
<div class="chatMeta">
|
|
<span>{{ active_conversation.members|length }} integrante(s)</span>
|
|
<span>•</span>
|
|
<span>{% if active_conversation.is_group %}Grupo{% else %}Chat privado{% endif %}</span>
|
|
{% if active_conversation.institution %}
|
|
<span>•</span>
|
|
<span>{{ active_conversation.institution.display_name }}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="userListStack" title="Integrantes">
|
|
{% set shown_members = active_conversation.members[:4] %}
|
|
{% for member in shown_members %}
|
|
<div class="memberAvatarCircle">{{ member.user.full_name[:2]|upper if member.user and member.user.full_name else 'US' }}</div>
|
|
{% endfor %}
|
|
{% if active_conversation.members|length > 4 %}
|
|
<div class="memberCounter">+{{ active_conversation.members|length - 4 }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<button type="button" class="chatToolbarBtn" data-bs-toggle="modal" data-bs-target="#newChatModal" title="Nueva conversación">
|
|
<i class="bi bi-three-dots"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="chatMessagesWrap" id="chatMessagesContainer">
|
|
{% for message in active_messages %}
|
|
{% set sender_name = message.sender.full_name if message.sender else 'Sistema' %}
|
|
<div class="chatRow {% if message.sender_id == current_user.id %}me{% endif %}" data-message-id="{{ message.id }}">
|
|
<div class="chatBubbleWrap">
|
|
<div class="chatAvatarCircle">{{ sender_name[:2]|upper }}</div>
|
|
<div class="commentCard">
|
|
<div class="commentHeader">
|
|
<span class="commentName">{{ 'Vos' if message.sender_id == current_user.id else sender_name }}</span>
|
|
<span class="commentTime">{{ message.created_at.strftime('%H:%M') }}</span>
|
|
</div>
|
|
<div class="commentBody">{{ message.body }}</div>
|
|
{% for att in message.attachments %}
|
|
<a href="{{ url_for('static', filename=att.file_path) }}" class="attachmentPill" target="_blank">
|
|
<i class="bi bi-paperclip"></i> {{ att.filename }}
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div class="chatComposer">
|
|
<form method="post" action="{{ url_for('admin_chat') }}" enctype="multipart/form-data" id="chatForm">
|
|
<input type="hidden" name="action" value="send_message">
|
|
<input type="hidden" name="conversation_id" value="{{ active_conversation.id }}">
|
|
<div class="composerBox">
|
|
<textarea class="form-control" name="body" id="chatBody" rows="3" placeholder="Escribí tu mensaje..."></textarea>
|
|
<div class="composerBottom pt-2">
|
|
<label class="composerFile mb-0">
|
|
<i class="bi bi-paperclip"></i>
|
|
<span>Adjuntar archivo</span>
|
|
<input type="file" class="form-control form-control-sm" name="chat_file" id="chatFileInput">
|
|
</label>
|
|
<button class="btn btn-primary px-4" id="chatSendButton">
|
|
<i class="bi bi-send me-1"></i> Enviar
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-chat-state">
|
|
<div class="bigIcon"><i class="bi bi-chat-dots"></i></div>
|
|
<h3 class="mb-2">Seleccioná una conversación</h3>
|
|
<p class="mb-4">Podés abrir un chat privado o crear un grupo desde el botón +.</p>
|
|
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#newChatModal">
|
|
<i class="bi bi-plus-lg me-1"></i> Nueva conversación
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal fade" id="newChatModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered">
|
|
<div class="modal-content border-0 shadow">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title fw-bold">Nueva conversación</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<ul class="nav nav-pills nav-fill mb-4" id="chatTab" role="tablist">
|
|
<li class="nav-item">
|
|
<button class="nav-link active" id="private-tab" data-bs-toggle="pill" data-bs-target="#private" type="button">Chat Privado</button>
|
|
</li>
|
|
<li class="nav-item">
|
|
<button class="nav-link" id="group-tab" data-bs-toggle="pill" data-bs-target="#group" type="button">Nuevo Grupo</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<div class="tab-content">
|
|
<div class="tab-pane fade show active" id="private">
|
|
<form method="post" action="{{ url_for('admin_chat') }}">
|
|
<input type="hidden" name="action" value="start_private">
|
|
<div class="mb-3">
|
|
<label class="form-label">Usuario</label>
|
|
<select class="form-select" name="target_user_id" required>
|
|
<option value="">Seleccionar...</option>
|
|
{% for user in available_users %}
|
|
<option value="{{ user.id }}">{{ user.full_name }} ({{ user.role }}{% if user.institution %} · {{ user.institution.display_name }}{% else %} · plataforma{% endif %})</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary w-100">Abrir chat</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="tab-pane fade" id="group">
|
|
<form method="post" action="{{ url_for('admin_chat') }}">
|
|
<input type="hidden" name="action" value="create_group">
|
|
<div class="mb-3">
|
|
<label class="form-label">Nombre del grupo</label>
|
|
<input class="form-control" name="title" required>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Integrantes</label>
|
|
<select class="form-select" name="member_ids" multiple required size="5">
|
|
{% for user in available_users %}
|
|
<option value="{{ user.id }}">{{ user.full_name }}{% if user.institution %} · {{ user.institution.display_name }}{% else %} · plataforma{% endif %}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<button type="submit" class="btn btn-outline-primary w-100">Crear grupo</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
(function(){
|
|
const appRoot = document.getElementById('messengerApp');
|
|
if (!appRoot) return;
|
|
|
|
const currentUserId = String(appRoot.dataset.currentUserId || '');
|
|
const currentConversationId = String(appRoot.dataset.activeConversationId || '');
|
|
const chatBaseUrl = String(appRoot.dataset.chatBaseUrl || '/chat');
|
|
|
|
const searchInput = document.getElementById('conversationSearch');
|
|
const conversationList = document.getElementById('conversationList');
|
|
const messagesContainer = document.getElementById('chatMessagesContainer');
|
|
const chatForm = document.getElementById('chatForm');
|
|
const chatBody = document.getElementById('chatBody');
|
|
const chatSendButton = document.getElementById('chatSendButton');
|
|
|
|
const chatSidebarToggle = document.getElementById('chatSidebarToggle');
|
|
const chatSidebarToggleInline = document.getElementById('chatSidebarToggleInline');
|
|
const chatSidebarClose = document.getElementById('chatSidebarClose');
|
|
const chatSidebarBackdrop = document.getElementById('chatSidebarBackdrop');
|
|
|
|
const knownMessages = new Set(Array.from(document.querySelectorAll('[data-message-id]')).map(el => String(el.dataset.messageId)));
|
|
let markReadTimer = null;
|
|
|
|
function isMobileChat() {
|
|
return window.innerWidth < 992;
|
|
}
|
|
|
|
function openChatSidebar() {
|
|
if (!isMobileChat()) return;
|
|
appRoot.classList.add('chat-sidebar-open');
|
|
document.body.classList.add('sidebar-lock');
|
|
}
|
|
|
|
function closeChatSidebar() {
|
|
appRoot.classList.remove('chat-sidebar-open');
|
|
document.body.classList.remove('sidebar-lock');
|
|
}
|
|
|
|
if (chatSidebarToggle) {
|
|
chatSidebarToggle.addEventListener('click', openChatSidebar);
|
|
}
|
|
|
|
if (chatSidebarToggleInline) {
|
|
chatSidebarToggleInline.addEventListener('click', openChatSidebar);
|
|
}
|
|
|
|
if (chatSidebarClose) {
|
|
chatSidebarClose.addEventListener('click', closeChatSidebar);
|
|
}
|
|
|
|
if (chatSidebarBackdrop) {
|
|
chatSidebarBackdrop.addEventListener('click', closeChatSidebar);
|
|
}
|
|
|
|
window.addEventListener('resize', function () {
|
|
if (!isMobileChat()) {
|
|
closeChatSidebar();
|
|
}
|
|
});
|
|
|
|
if (conversationList) {
|
|
conversationList.addEventListener('click', function(ev){
|
|
const item = ev.target.closest('.convItem');
|
|
if (item && isMobileChat()) {
|
|
closeChatSidebar();
|
|
}
|
|
});
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value || '').replace(/[&<>"']/g, function(ch){
|
|
return ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]);
|
|
});
|
|
}
|
|
|
|
function initials(name) {
|
|
const parts = String(name || 'Chat').trim().split(/\s+/).filter(Boolean);
|
|
return ((parts[0] ? parts[0][0] : 'C') + (parts[1] ? parts[1][0] : '')).toUpperCase();
|
|
}
|
|
|
|
function scrollToBottom() {
|
|
if (messagesContainer) {
|
|
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
|
}
|
|
}
|
|
|
|
function attachmentHtml(att) {
|
|
return `<a href="${escapeHtml(att.url)}" class="attachmentPill" target="_blank"><i class="bi bi-paperclip"></i> ${escapeHtml(att.filename || 'Adjunto')}</a>`;
|
|
}
|
|
|
|
function messageHtml(message) {
|
|
const isMine = String(message.sender_id) === currentUserId;
|
|
const body = escapeHtml(message.body || '');
|
|
const attachments = (message.attachments || []).map(attachmentHtml).join('');
|
|
return `<div class="chatRow ${isMine ? 'me' : ''}" data-message-id="${message.message_id}"><div class="chatBubbleWrap"><div class="chatAvatarCircle">${initials(message.sender_name || 'Sistema')}</div><div class="commentCard"><div class="commentHeader"><span class="commentName">${isMine ? 'Vos' : escapeHtml(message.sender_name || 'Sistema')}</span><span class="commentTime">${escapeHtml(message.created_time || '')}</span></div><div class="commentBody">${body.replace(/\n/g, '<br>')}</div>${attachments}</div></div></div>`;
|
|
}
|
|
|
|
function appendMessage(message) {
|
|
if (!messagesContainer || !message || knownMessages.has(String(message.message_id))) return;
|
|
knownMessages.add(String(message.message_id));
|
|
messagesContainer.insertAdjacentHTML('beforeend', messageHtml(message));
|
|
scrollToBottom();
|
|
}
|
|
|
|
function conversationItemHtml(conversation, active) {
|
|
const unread = Number(conversation.unread || 0);
|
|
const search = `${conversation.display_name || ''} ${conversation.last_message || ''}`.toLowerCase();
|
|
const href = `${chatBaseUrl}?conversation_id=${conversation.id}`;
|
|
return `<a class="convItem ${active ? 'active' : ''}" href="${href}" data-conversation-id="${conversation.id}" data-search="${escapeHtml(search)}"><div class="convOverall"><div class="convImg ${conversation.is_group ? 'group' : ''}">${initials(conversation.display_name || 'Chat')}</div><div class="convContent"><div class="convSubrow"><span class="convName">${escapeHtml(conversation.display_name || 'Chat')}</span><span class="convDate">${escapeHtml(conversation.last_message_short_time || '')}</span></div><div class="convSubrow"><span class="convMessage">${escapeHtml(conversation.last_message || 'Sin mensajes')}</span><span class="convCounter ${unread ? '' : 'd-none'}">${unread}</span></div></div></div></a>`;
|
|
}
|
|
|
|
function upsertConversation(conversation) {
|
|
if (!conversationList || !conversation) return;
|
|
const existing = conversationList.querySelector(`[data-conversation-id="${conversation.id}"]`);
|
|
const active = String(conversation.id) === currentConversationId;
|
|
if (existing) existing.remove();
|
|
conversationList.insertAdjacentHTML('afterbegin', conversationItemHtml(conversation, active));
|
|
}
|
|
|
|
function markRead() {
|
|
if (!currentConversationId) return;
|
|
const fd = new FormData();
|
|
fd.append('conversation_id', currentConversationId);
|
|
fetch('/api/chat/mark-read', {
|
|
method:'POST',
|
|
body:fd,
|
|
headers:{'X-Requested-With':'XMLHttpRequest'}
|
|
}).catch(() => {});
|
|
}
|
|
|
|
function scheduleMarkRead() {
|
|
clearTimeout(markReadTimer);
|
|
markReadTimer = setTimeout(markRead, 400);
|
|
}
|
|
|
|
if (searchInput && conversationList) {
|
|
searchInput.addEventListener('input', function() {
|
|
const term = (searchInput.value || '').toLowerCase().trim();
|
|
conversationList.querySelectorAll('.convItem').forEach(function(item){
|
|
item.style.display = !term || (item.dataset.search || '').includes(term) ? '' : 'none';
|
|
});
|
|
});
|
|
}
|
|
|
|
if (chatForm && chatSendButton) {
|
|
chatForm.addEventListener('submit', async function(ev){
|
|
ev.preventDefault();
|
|
const fileField = chatForm.querySelector('input[name="chat_file"]');
|
|
const bodyText = (chatBody && chatBody.value || '').trim();
|
|
|
|
if (!bodyText && !(fileField && fileField.files && fileField.files.length)) return;
|
|
|
|
chatSendButton.disabled = true;
|
|
chatForm.classList.add('sending-state');
|
|
|
|
try {
|
|
const payload = new FormData(chatForm);
|
|
const res = await fetch('/api/chat/send', {
|
|
method:'POST',
|
|
body:payload,
|
|
headers:{'X-Requested-With':'XMLHttpRequest'}
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok || !data.ok) throw new Error(data.error || 'No se pudo enviar el mensaje.');
|
|
|
|
if (chatBody) chatBody.value = '';
|
|
if (fileField) fileField.value = '';
|
|
scheduleMarkRead();
|
|
} catch (error) {
|
|
alert(error.message || 'No se pudo enviar el mensaje.');
|
|
} finally {
|
|
chatSendButton.disabled = false;
|
|
chatForm.classList.remove('sending-state');
|
|
if (chatBody) chatBody.focus();
|
|
}
|
|
});
|
|
}
|
|
|
|
document.addEventListener('appchat:notify', function(event){
|
|
const data = event.detail || {};
|
|
|
|
if (data.conversation) {
|
|
upsertConversation(data.conversation);
|
|
}
|
|
|
|
if (data.message && String(data.message.conversation_id) === currentConversationId) {
|
|
appendMessage(data.message);
|
|
scheduleMarkRead();
|
|
}
|
|
});
|
|
|
|
if (window.AppChat && window.AppChat.socket && currentConversationId) {
|
|
window.AppChat.socket.emit('join', { room: `chat_${currentConversationId}` });
|
|
}
|
|
|
|
if (messagesContainer) {
|
|
scrollToBottom();
|
|
scheduleMarkRead();
|
|
|
|
document.addEventListener('visibilitychange', function(){
|
|
if (!document.hidden) scheduleMarkRead();
|
|
});
|
|
|
|
window.addEventListener('focus', scheduleMarkRead);
|
|
}
|
|
})();
|
|
</script>
|
|
{% endblock %} |