481 lines
16 KiB
JavaScript
481 lines
16 KiB
JavaScript
// 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 = `<em class="text-primary">${userName} печатает...</em>`;
|
|
|
|
// 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 = '<i class="bi-pin-angle-fill"></i>';
|
|
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 = `
|
|
<button onclick="togglePinChat(${chatId}, event)">
|
|
<i class="bi-pin-angle${isPinned ? '-fill' : ''}"></i>
|
|
${isPinned ? 'Открепить' : 'Закрепить'}
|
|
</button>
|
|
<button onclick="toggleNotifications(${chatId}, event)">
|
|
<i class="bi-bell-slash"></i>
|
|
Выключить уведомления
|
|
</button>
|
|
<button onclick="hideChat(${chatId}, event)">
|
|
<i class="bi-eye-slash"></i>
|
|
Скрыть чат
|
|
</button>
|
|
<hr class="my-1">
|
|
<button onclick="markAsRead(${chatId}, event)">
|
|
<i class="bi-check-all"></i>
|
|
Отметить прочитанным
|
|
</button>
|
|
`;
|
|
|
|
document.body.appendChild(menu);
|
|
|
|
// Close on click outside
|
|
setTimeout(() => {
|
|
document.addEventListener('click', function closeMenu() {
|
|
menu.remove();
|
|
document.removeEventListener('click', closeMenu);
|
|
});
|
|
}, 10);
|
|
}
|
|
|
|
window.toggleNotifications = function(chatId, event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
fetch(`/communications/api/chat/${chatId}/notifications/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'X-CSRFToken': getCsrfToken()
|
|
},
|
|
body: 'enabled=false'
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.ok) {
|
|
console.log('Notifications toggled');
|
|
}
|
|
});
|
|
};
|
|
|
|
window.hideChat = function(chatId, event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
if (!confirm('Скрыть этот чат из списка?')) return;
|
|
|
|
// TODO: Implement hide chat API
|
|
console.log('Hide chat:', chatId);
|
|
};
|
|
|
|
window.markAsRead = function(chatId, event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
fetch(`/communications/chats/${chatId}/mark-read/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'X-CSRFToken': getCsrfToken()
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.ok) {
|
|
const chatRow = document.querySelector(`[data-chat-id="${chatId}"]`);
|
|
const unreadBadge = chatRow.querySelector('[data-unread]');
|
|
if (unreadBadge) {
|
|
unreadBadge.classList.add('d-none');
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
// ==================== Search & Filter ====================
|
|
|
|
const searchInput = document.getElementById('chatSearch');
|
|
if (searchInput) {
|
|
searchInput.addEventListener('input', function(e) {
|
|
const query = e.target.value.toLowerCase().trim();
|
|
|
|
document.querySelectorAll('.chat-row').forEach(row => {
|
|
const haystack = row.dataset.haystack?.toLowerCase() || '';
|
|
const match = haystack.includes(query);
|
|
row.style.display = match ? '' : 'none';
|
|
});
|
|
|
|
updateEmptyNotes();
|
|
});
|
|
}
|
|
|
|
function updateEmptyNotes() {
|
|
document.querySelectorAll('.chat-section').forEach(section => {
|
|
const listContainer = section.querySelector('.list-chats');
|
|
const visibleChats = Array.from(listContainer.children).filter(
|
|
row => row.style.display !== 'none'
|
|
);
|
|
|
|
const emptyNote = section.querySelector('[data-empty-note]');
|
|
if (emptyNote) {
|
|
emptyNote.style.display = visibleChats.length === 0 ? 'block' : 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
// ==================== Utilities ====================
|
|
|
|
function getCsrfToken() {
|
|
return document.querySelector('[name=csrfmiddlewaretoken]')?.value || '';
|
|
}
|
|
|
|
// ==================== Init ====================
|
|
|
|
connectWebSocket();
|
|
initDropdownMenus();
|
|
updateEmptyNotes();
|
|
|
|
// Sort pinned chats to top on load
|
|
document.querySelectorAll('.list-chats').forEach(container => {
|
|
const pinned = Array.from(container.querySelectorAll('.chat-row.pinned'));
|
|
pinned.forEach(row => moveToPinnedSection(row));
|
|
});
|
|
|
|
// ==================== Dropdown Menu Handlers ====================
|
|
|
|
function initDropdownMenus() {
|
|
// Обновление текста "Закрепить"/"Открепить" при открытии dropdown
|
|
document.addEventListener('show.bs.dropdown', function(e) {
|
|
const dropdown = e.target;
|
|
const chatRow = dropdown.closest('.chat-row');
|
|
if (!chatRow) return;
|
|
|
|
const isPinned = chatRow.classList.contains('pinned');
|
|
const pinTextElement = dropdown.parentElement.querySelector('[data-pin-text]');
|
|
if (pinTextElement) {
|
|
pinTextElement.textContent = isPinned ? 'Открепить' : 'Закрепить';
|
|
}
|
|
});
|
|
|
|
// Обработка действий в выпадающем меню
|
|
document.addEventListener('click', function(e) {
|
|
const actionLink = e.target.closest('[data-action]');
|
|
if (!actionLink) return;
|
|
|
|
e.preventDefault();
|
|
const action = actionLink.dataset.action;
|
|
const chatRow = actionLink.closest('.chat-row');
|
|
if (!chatRow) return;
|
|
|
|
const chatId = chatRow.dataset.chatId;
|
|
|
|
switch(action) {
|
|
case 'pin':
|
|
togglePinChat(chatId, chatRow);
|
|
break;
|
|
case 'notifications':
|
|
toggleNotifications(chatId, chatRow);
|
|
break;
|
|
case 'edit':
|
|
editChat(chatId);
|
|
break;
|
|
case 'manage':
|
|
manageChat(chatId);
|
|
break;
|
|
case 'delete':
|
|
deleteChat(chatId, chatRow);
|
|
break;
|
|
}
|
|
|
|
// Закрываем dropdown
|
|
const dropdownBtn = actionLink.closest('.dropdown-menu')?.previousElementSibling;
|
|
if (dropdownBtn) {
|
|
const dropdown = bootstrap.Dropdown.getInstance(dropdownBtn);
|
|
if (dropdown) dropdown.hide();
|
|
}
|
|
});
|
|
}
|
|
|
|
function toggleNotifications(chatId, chatRow) {
|
|
// TODO: реализовать API запрос
|
|
console.log('Toggle notifications for chat', chatId);
|
|
alert('Управление уведомлениями будет доступно в следующей версии');
|
|
}
|
|
|
|
function editChat(chatId) {
|
|
// TODO: создать страницу редактирования чата
|
|
alert('Редактирование чатов будет доступно в следующей версии');
|
|
// window.location.href = `/communications/chat/${chatId}/edit/`;
|
|
}
|
|
|
|
function manageChat(chatId) {
|
|
// TODO: создать страницу управления чатом
|
|
alert('Управление чатом (участники, права) будет доступно в следующей версии');
|
|
// window.location.href = `/communications/chat/${chatId}/manage/`;
|
|
}
|
|
|
|
function deleteChat(chatId, chatRow) {
|
|
// TODO: реализовать API endpoint для удаления чата
|
|
alert('Удаление чатов будет доступно в следующей версии');
|
|
return;
|
|
|
|
/* Закомментировано до реализации API
|
|
if (!confirm('Вы уверены, что хотите удалить этот чат? Это действие необратимо.')) {
|
|
return;
|
|
}
|
|
|
|
fetch(`/communications/api/chat/${chatId}/delete/`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'X-CSRFToken': getCsrfToken()
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.ok) {
|
|
// Удаляем строку из списка
|
|
chatRow.style.transition = 'opacity 0.3s';
|
|
chatRow.style.opacity = '0';
|
|
setTimeout(() => chatRow.remove(), 300);
|
|
} else {
|
|
alert('Ошибка при удалении чата: ' + (data.error || 'Неизвестная ошибка'));
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Delete error:', error);
|
|
alert('Ошибка при удалении чата');
|
|
});
|
|
*/
|
|
}
|
|
|
|
})();
|