/**
* @fileoverview Message Selection - выделение и пересылка сообщений
* @module components/messageSelection
*/
/**
* Класс для управления выделением и пересылкой сообщений
*/
export class MessageSelection {
/**
* @param {Object} options - Опции конфигурации
* @param {string} options.chatId - ID текущего чата
* @param {string} options.scrollContainerId - ID контейнера сообщений
* @param {string} options.currentUserId - ID текущего пользователя
* @param {Function} options.onForward - Callback при успешной пересылке
*/
constructor(options) {
console.log('[MessageSelection] Constructor called with options:', options);
this.chatId = options.chatId;
this.scrollContainer = document.getElementById(options.scrollContainerId);
this.currentUserId = options.currentUserId;
this.onForward = options.onForward || (() => {});
this.selectedMessages = new Set();
this.selectionMode = false;
console.log('[MessageSelection] scrollContainer found:', !!this.scrollContainer);
this.init();
}
/**
* Инициализация
*/
init() {
console.log('[MessageSelection] Initializing...');
this.createActionBar();
this.attachEventListeners();
// Слушаем события контекстного меню
window.addEventListener('message-context-menu', (e) => {
console.log('[MessageSelection] message-context-menu event received:', e.detail);
const action = e.detail?.action;
const messageElement = e.detail?.messageElement;
if (action === 'select' && messageElement) {
console.log('[MessageSelection] Entering selection mode for message:', messageElement.dataset.messageId);
this.enterSelectionMode(messageElement);
}
});
// Слушаем события пересылки одного сообщения
window.addEventListener('message-forward-single', (e) => {
console.log('[MessageSelection] message-forward-single event received:', e.detail);
const messageId = e.detail?.messageId;
if (messageId) {
console.log('[MessageSelection] Forwarding single message:', messageId);
this.forwardSingleMessage(messageId);
}
});
console.log('[MessageSelection] Initialized successfully');
}
/**
* Создает панель действий внизу экрана
*/
createActionBar() {
const bar = document.createElement('div');
bar.className = 'selection-actions-bar';
bar.id = 'selectionActionsBar';
bar.innerHTML = `
0 выбрано
`;
document.body.appendChild(bar);
this.actionBar = bar;
this.countElement = bar.querySelector('#selectionCount');
// Обработчики кнопок
bar.querySelector('#btnCancelSelection').addEventListener('click', () => {
this.exitSelectionMode();
});
bar.querySelector('#btnForwardSelected').addEventListener('click', () => {
this.showForwardDialog();
});
bar.querySelector('#btnDeleteSelected').addEventListener('click', () => {
this.deleteSelected();
});
}
/**
* Подключает обработчики событий
*/
attachEventListeners() {
// Делегирование событий для чекбоксов и сообщений
this.scrollContainer.addEventListener('click', (e) => {
if (!this.selectionMode) return;
const checkbox = e.target.closest('.msg-select-checkbox');
if (checkbox) {
const messageElement = checkbox.closest('.msg');
this.toggleMessageSelection(messageElement);
return;
}
const messageElement = e.target.closest('.msg');
if (messageElement) {
this.toggleMessageSelection(messageElement);
}
});
// Долгое нажатие отключено - режим выбора активируется только через кнопку "Выбрать" в меню
// let longPressTimer = null;
//
// this.scrollContainer.addEventListener('touchstart', (e) => {
// const messageElement = e.target.closest('.msg');
// if (!messageElement || this.selectionMode) return;
//
// longPressTimer = setTimeout(() => {
// this.enterSelectionMode(messageElement);
// navigator.vibrate?.(50); // Вибрация на мобильных
// }, 500);
// });
//
// this.scrollContainer.addEventListener('touchend', () => {
// clearTimeout(longPressTimer);
// });
//
// this.scrollContainer.addEventListener('touchmove', () => {
// clearTimeout(longPressTimer);
// });
// ESC для выхода из режима
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.selectionMode) {
this.exitSelectionMode();
}
});
}
/**
* Входит в режим выделения
* @param {HTMLElement} firstMessage - Первое выделенное сообщение
*/
enterSelectionMode(firstMessage) {
this.selectionMode = true;
this.scrollContainer.classList.add('selection-mode');
this.actionBar.classList.add('active');
// Добавляем чекбоксы ко всем сообщениям
const messages = this.scrollContainer.querySelectorAll('.msg');
messages.forEach(msg => {
if (!msg.querySelector('.msg-select-checkbox')) {
const checkbox = document.createElement('div');
checkbox.className = 'msg-select-checkbox';
msg.style.position = 'relative';
msg.appendChild(checkbox);
}
});
// Выделяем первое сообщение
if (firstMessage) {
this.toggleMessageSelection(firstMessage);
}
}
/**
* Выходит из режима выделения
*/
exitSelectionMode() {
this.selectionMode = false;
this.selectedMessages.clear();
this.scrollContainer.classList.remove('selection-mode');
this.actionBar.classList.remove('active');
// Убираем чекбоксы и выделение
const messages = this.scrollContainer.querySelectorAll('.msg');
messages.forEach(msg => {
msg.classList.remove('selected');
const checkbox = msg.querySelector('.msg-select-checkbox');
if (checkbox) {
checkbox.remove();
}
});
this.updateCount();
}
/**
* Переключает выделение сообщения
* @param {HTMLElement} messageElement - Элемент сообщения
*/
toggleMessageSelection(messageElement) {
if (!messageElement) return;
const messageId = messageElement.dataset.id;
if (!messageId) return;
const checkbox = messageElement.querySelector('.msg-select-checkbox');
if (this.selectedMessages.has(messageId)) {
this.selectedMessages.delete(messageId);
messageElement.classList.remove('selected');
checkbox?.classList.remove('selected');
} else {
this.selectedMessages.add(messageId);
messageElement.classList.add('selected');
checkbox?.classList.add('selected');
}
this.updateCount();
// Автовыход если ничего не выбрано
if (this.selectedMessages.size === 0) {
this.exitSelectionMode();
}
}
/**
* Обновляет счетчик выделенных сообщений
*/
updateCount() {
const count = this.selectedMessages.size;
this.countElement.textContent = count;
// Показываем кнопку удаления только для своих сообщений
const deleteBtn = this.actionBar.querySelector('#btnDeleteSelected');
const canDelete = this.canDeleteSelected();
deleteBtn.style.display = canDelete ? 'block' : 'none';
}
/**
* Проверяет, можно ли удалить выделенные сообщения
* @returns {boolean}
*/
canDeleteSelected() {
for (const messageId of this.selectedMessages) {
const messageElement = this.scrollContainer.querySelector(`[data-id="${messageId}"]`);
const authorId = messageElement?.dataset.authorId;
if (authorId !== this.currentUserId) {
return false;
}
}
return true;
}
/**
* Пересылает одно сообщение (без входа в режим выбора)
* @param {string|number} messageId - ID сообщения для пересылки
*/
async forwardSingleMessage(messageId) {
console.log('forwardSingleMessage called with:', messageId);
// Временно добавляем сообщение в selectedMessages
const wasInSelectionMode = this.selectionMode;
const originalSelection = new Set(this.selectedMessages);
this.selectedMessages.clear();
this.selectedMessages.add(String(messageId));
console.log('selectedMessages after add:', this.selectedMessages);
// Создаем модальное окно
const modal = this.createForwardModal();
document.body.appendChild(modal);
// Загружаем список чатов
await this.loadChats(modal);
// Показываем модальное окно
const bsModal = new bootstrap.Modal(modal);
bsModal.show();
// Восстанавливаем состояние после закрытия модала
modal.addEventListener('hidden.bs.modal', () => {
modal.remove();
if (!wasInSelectionMode) {
this.selectedMessages = originalSelection;
}
});
}
/**
* Показывает диалог выбора чата для пересылки
*/
async showForwardDialog() {
if (this.selectedMessages.size === 0) return;
// Создаем модальное окно
const modal = this.createForwardModal();
document.body.appendChild(modal);
// Загружаем список чатов
await this.loadChats(modal);
// Показываем модальное окно
const bsModal = new bootstrap.Modal(modal);
bsModal.show();
// Удаляем после закрытия
modal.addEventListener('hidden.bs.modal', () => {
modal.remove();
});
}
/**
* Создает модальное окно для пересылки
* @returns {HTMLElement}
*/
createForwardModal() {
const modal = document.createElement('div');
modal.className = 'modal fade chat-forward-modal';
modal.id = 'forwardModal';
modal.tabIndex = -1;
modal.innerHTML = `
`;
// Поиск
const searchInput = modal.querySelector('#chatSearchInput');
searchInput.addEventListener('input', (e) => {
this.filterChats(modal, e.target.value);
});
return modal;
}
/**
* Загружает список чатов
* @param {HTMLElement} modal - Модальное окно
*/
async loadChats(modal) {
const container = modal.querySelector('#chatListContainer');
try {
const response = await fetch('/api/v1/communications/chats/', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin'
});
if (!response.ok) {
const errorText = await response.text();
console.error('Server error:', response.status, errorText);
throw new Error(`Failed to load chats: ${response.status}`);
}
const data = await response.json();
const chats = data.results || data;
if (!Array.isArray(chats)) {
console.error('Invalid response format:', data);
throw new Error('Invalid response format');
}
// Исключаем текущий чат
const filteredChats = chats.filter(chat => String(chat.id) !== String(this.chatId));
if (filteredChats.length === 0) {
container.innerHTML = `
`;
return;
}
container.innerHTML = filteredChats.map(chat => this.renderChatItem(chat)).join('');
// Обработчики клика
container.querySelectorAll('.chat-list-item').forEach(item => {
item.addEventListener('click', () => {
const targetChatId = item.dataset.chatId;
this.forwardToChat(targetChatId, modal);
});
});
} catch (error) {
console.error('Failed to load chats:', error);
container.innerHTML = `
Ошибка загрузки чатов
`;
}
}
/**
* Рендерит элемент чата
* @param {Object} chat - Данные чата
* @returns {string}
*/
renderChatItem(chat) {
const avatar = chat.avatar || '';
const name = chat.name || 'Без названия';
const subtitle = chat.last_message?.content || 'Нет сообщений';
const type = chat.type || 'private';
// Иконки для типов чатов
const typeIcons = {
'private': 'bi-person-fill',
'group': 'bi-people-fill',
'channel': 'bi-broadcast',
'department': 'bi-diagram-3',
'announcement': 'bi-megaphone',
'global': 'bi-globe'
};
const icon = typeIcons[type] || 'bi-chat-dots';
return `
${avatar ? `

` : ''}
${this.escapeHtml(name)}
${this.escapeHtml(subtitle)}
`;
}
/**
* Фильтрует чаты по поисковому запросу
* @param {HTMLElement} modal - Модальное окно
* @param {string} query - Поисковый запрос
*/
filterChats(modal, query) {
const container = modal.querySelector('#chatListContainer');
const items = container.querySelectorAll('.chat-list-item');
const lowerQuery = query.toLowerCase();
items.forEach(item => {
const name = item.querySelector('.chat-list-name').textContent.toLowerCase();
item.style.display = name.includes(lowerQuery) ? 'flex' : 'none';
});
}
/**
* Пересылает сообщения в выбранный чат
* @param {string} targetChatId - ID целевого чата
* @param {HTMLElement} modal - Модальное окно
*/
async forwardToChat(targetChatId, modal) {
const messageIds = Array.from(this.selectedMessages);
console.log('forwardToChat:', {
selectedMessages: this.selectedMessages,
messageIds: messageIds,
targetChatId: targetChatId
});
try {
const response = await fetch('/api/v1/communications/messages/forward/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCsrfToken()
},
body: JSON.stringify({
message_ids: messageIds,
target_chat_id: targetChatId
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Failed to forward messages');
}
// Закрываем модальное окно
const bsModal = bootstrap.Modal.getInstance(modal);
bsModal.hide();
// Выходим из режима выделения
this.exitSelectionMode();
// Показываем уведомление
this.showToast('success', `${messageIds.length} ${this.getMessageWord(messageIds.length)} переслано`);
// Callback
this.onForward(messageIds, targetChatId);
} catch (error) {
console.error('Failed to forward messages:', error);
this.showToast('danger', 'Ошибка при пересылке сообщений');
}
}
/**
* Удаляет выделенные сообщения
*/
async deleteSelected() {
if (!this.canDeleteSelected()) return;
const messageIds = Array.from(this.selectedMessages);
if (!confirm(`Удалить ${messageIds.length} ${this.getMessageWord(messageIds.length)}?`)) {
return;
}
try {
const response = await fetch('/api/v1/communications/messages/bulk-delete/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': this.getCsrfToken()
},
body: JSON.stringify({
message_ids: messageIds
})
});
if (!response.ok) {
throw new Error('Failed to delete messages');
}
// Удаляем из DOM
messageIds.forEach(id => {
const element = this.scrollContainer.querySelector(`[data-id="${id}"]`);
element?.remove();
});
// Выходим из режима выделения
this.exitSelectionMode();
this.showToast('success', 'Сообщения удалены');
} catch (error) {
console.error('Failed to delete messages:', error);
this.showToast('danger', 'Ошибка при удалении сообщений');
}
}
/**
* Получает CSRF токен
* @returns {string}
*/
getCsrfToken() {
return document.querySelector('[name=csrfmiddlewaretoken]')?.value || '';
}
/**
* Показывает toast уведомление
* @param {string} type - Тип (success, danger, warning, info)
* @param {string} message - Текст сообщения
*/
showToast(type, message) {
const toast = document.createElement('div');
toast.className = `alert alert-${type} position-fixed bottom-0 end-0 m-3`;
toast.style.zIndex = '9999';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
/**
* Возвращает правильную форму слова "сообщение"
* @param {number} count - Количество
* @returns {string}
*/
getMessageWord(count) {
const lastDigit = count % 10;
const lastTwoDigits = count % 100;
if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
return 'сообщений';
}
if (lastDigit === 1) {
return 'сообщение';
}
if (lastDigit >= 2 && lastDigit <= 4) {
return 'сообщения';
}
return 'сообщений';
}
/**
* Экранирует HTML
* @param {string} text - Текст
* @returns {string}
*/
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// Export для использования в других модулях
export default MessageSelection;