// Enhanced Chat List with pinning, avatars, and real-time updates (function() { 'use strict'; let ws = null; let reconnectAttempts = 0; const MAX_RECONNECT = 5; // ==================== WebSocket ==================== function connectWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws/chats/`; ws = new WebSocket(wsUrl); ws.onopen = () => { console.log('Chat List WebSocket connected'); reconnectAttempts = 0; }; ws.onmessage = (e) => { const data = JSON.parse(e.data); handleWebSocketMessage(data); }; ws.onerror = (e) => { console.error('WebSocket error:', e); }; ws.onclose = () => { console.log('WebSocket closed'); if (reconnectAttempts < MAX_RECONNECT) { reconnectAttempts++; setTimeout(connectWebSocket, 2000 * reconnectAttempts); } }; } function handleWebSocketMessage(data) { switch(data.type) { case 'list_update': updateChatLastMessage(data.chat_id, data.message); break; case 'message_edited': updateChatLastMessage(data.chat_id, data.message); break; case 'user_typing': showChatTyping(data.chat_id, data.user_name); break; } } function updateChatLastMessage(chatId, message) { const chatRow = document.querySelector(`[data-chat-id="${chatId}"]`); if (!chatRow) return; // Update timestamp const lastTime = chatRow.querySelector('[data-last-time]'); if (lastTime) { const date = new Date(message.created_ts); lastTime.textContent = date.toLocaleString('ru', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } // Update author const lastAuthor = chatRow.querySelector('[data-last-author]'); if (lastAuthor) { lastAuthor.textContent = message.author_name; } // Update preview const lastPreview = chatRow.querySelector('[data-last-preview]'); if (lastPreview) { let preview = message.content; if (message.is_deleted) { preview = 'Сообщение удалено'; } else if (message.has_attachments) { preview = '📎 Вложение'; } lastPreview.textContent = preview.substring(0, 120); } // Update unread count (simplified - actual count needs server update) const unreadBadge = chatRow.querySelector('[data-unread]'); if (unreadBadge && message.author_id !== getCurrentUserId()) { unreadBadge.classList.remove('d-none'); } // Move to top if not pinned const isPinned = chatRow.classList.contains('pinned'); if (!isPinned) { moveToTop(chatRow); } // Flash animation chatRow.classList.add('updated'); setTimeout(() => chatRow.classList.remove('updated'), 1000); } function showChatTyping(chatId, userName) { const chatRow = document.querySelector(`[data-chat-id="${chatId}"]`); if (!chatRow) return; const feedSub = chatRow.querySelector('.card-subtitle'); if (!feedSub) return; // Save original content if (!feedSub.dataset.originalContent) { feedSub.dataset.originalContent = feedSub.innerHTML; } // Show typing feedSub.innerHTML = `${userName} печатает...`; // Hide after 3s clearTimeout(feedSub._typingTimeout); feedSub._typingTimeout = setTimeout(() => { if (feedSub.dataset.originalContent) { feedSub.innerHTML = feedSub.dataset.originalContent; delete feedSub.dataset.originalContent; } }, 3000); } function moveToTop(chatRow) { const parent = chatRow.parentElement; const firstChild = parent.firstElementChild; if (firstChild !== chatRow) { parent.insertBefore(chatRow, firstChild); } } function getCurrentUserId() { // Try to get from data attribute or similar return parseInt(document.body.dataset.userId || 0); } // ==================== Pin/Unpin ==================== window.togglePinChat = function(chatId, event) { event.preventDefault(); event.stopPropagation(); const chatRow = document.querySelector(`[data-chat-id="${chatId}"]`); const isPinned = chatRow.classList.contains('pinned'); fetch(`/api/v1/communications/chats/${chatId}/pin/`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': getCsrfToken() }, body: `pinned=${!isPinned}` }) .then(response => response.json()) .then(data => { if (data.ok) { if (data.pinned) { chatRow.classList.add('pinned'); addPinBadge(chatRow); moveToPinnedSection(chatRow); } else { chatRow.classList.remove('pinned'); removePinBadge(chatRow); moveToUnpinnedSection(chatRow); } } }) .catch(error => console.error('Pin error:', error)); }; function addPinBadge(chatRow) { if (chatRow.querySelector('.chat-pinned-badge')) return; const badge = document.createElement('div'); badge.className = 'chat-pinned-badge'; badge.innerHTML = ''; badge.title = 'Закреплено'; const feedMeta = chatRow.querySelector('.flex-grow-1'); feedMeta.style.position = 'relative'; feedMeta.appendChild(badge); } function removePinBadge(chatRow) { const badge = chatRow.querySelector('.chat-pinned-badge'); if (badge) badge.remove(); } function moveToPinnedSection(chatRow) { const parent = chatRow.parentElement; const firstUnpinned = Array.from(parent.children).find(row => !row.classList.contains('pinned') ); if (firstUnpinned) { parent.insertBefore(chatRow, firstUnpinned); } else { parent.appendChild(chatRow); } } function moveToUnpinnedSection(chatRow) { const parent = chatRow.parentElement; parent.appendChild(chatRow); } // ==================== Context Menu ==================== function initContextMenus() { document.querySelectorAll('.chat-row').forEach(chatRow => { chatRow.addEventListener('contextmenu', function(e) { e.preventDefault(); showContextMenu(e, this); }); }); } function showContextMenu(event, chatRow) { const chatId = chatRow.dataset.chatId; const isPinned = chatRow.classList.contains('pinned'); // Remove existing menu document.getElementById('chat-context-menu')?.remove(); const menu = document.createElement('div'); menu.id = 'chat-context-menu'; menu.className = 'context-menu'; menu.style.left = `${event.pageX}px`; menu.style.top = `${event.pageY}px`; menu.innerHTML = `