// backend/static/js/components/messageContextMenu.js
/**
* Контекстное меню для сообщений (как в Telegram/WhatsApp)
* - Мобилка: долгое нажатие
* - Десктоп: правый клик
*/
export class MessageContextMenu {
constructor(config = {}) {
console.log('[MessageContextMenu] Constructor called', config);
this.onReactionSelect = config.onReactionSelect || (() => {});
this.emojis = config.emojis || ['👍', '❤️', '😂', '😮', '�']; // 5 смайликов + кнопка = 6 элементов
this.currentUserId = config.currentUserId;
this.containerId = config.containerId || 'chatScroll';
this.activeMenu = null;
this.highlightedMessage = null; // Храним ссылку на подсвеченное сообщение
this.longPressTimer = null;
this.longPressDuration = 500; // 500ms для долгого нажатия
this.init();
this.attachToExistingMessages();
this.observeNewMessages();
}
init() {
// Закрытие меню при клике вне его
document.addEventListener('click', (e) => {
if (this.activeMenu &&
!e.target.closest('.message-context-menu') &&
!e.target.closest('.quick-reactions-panel')) {
this.removeHighlight(); // Убираем подсветку
this.closeMenu();
}
}, true); // Используем capture phase для приоритета
// Закрытие при Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.activeMenu) {
this.removeHighlight(); // Убираем подсветку
this.closeMenu();
}
});
// Закрытие при скролле
document.addEventListener('scroll', () => {
if (this.activeMenu) {
this.removeHighlight(); // Убираем подсветку
this.closeMenu();
}
}, true);
}
/**
* Подключить контекстное меню ко всем существующим сообщениям
*/
attachToExistingMessages() {
console.log('[MessageContextMenu] Attaching to existing messages in:', this.containerId);
const container = document.getElementById(this.containerId);
if (!container) {
console.warn('[MessageContextMenu] Container not found:', this.containerId);
return;
}
const messages = container.querySelectorAll('.msg[data-message-id]');
console.log('[MessageContextMenu] Found', messages.length, 'messages');
messages.forEach((msg) => {
if (!msg.dataset.contextMenuAttached) {
this.attachToMessage(msg);
msg.dataset.contextMenuAttached = 'true';
}
});
}
/**
* Наблюдать за новыми сообщениями через MutationObserver
*/
observeNewMessages() {
const container = document.getElementById(this.containerId);
if (!container) {
console.warn('[MessageContextMenu] Container not found for observer:', this.containerId);
return;
}
// РЕФАКТОРИНГ: MutationObserver УДАЛЕН
// Он дублировал обработку сообщений с событием chat:message-added
// Теперь используется ТОЛЬКО событийная модель - chat-detail-enhanced.js
// слушает chat:message-added и вызывает attachToMessage
console.log('[MessageContextMenu] Observer setup skipped - using event-based model only');
}
/**
* Подключить обработчики к сообщению
*/
attachToMessage(messageElement) {
const messageId = messageElement.dataset.messageId;
const authorId = parseInt(messageElement.dataset.authorId);
const isOwn = authorId === this.currentUserId;
console.log('[MessageContextMenu] attachToMessage called:', {
messageId,
authorId,
currentUserId: this.currentUserId,
isOwn,
alreadyAttached: messageElement.dataset.contextMenuAttached
});
// Десктоп: правый клик
messageElement.addEventListener('contextmenu', (e) => {
// Игнорируем клик на кнопках реакций и контейнере реакций
if (e.target.closest('.reaction-button') || e.target.closest('.message-reactions')) {
console.log('[MessageContextMenu] Ignoring contextmenu on reaction button/container');
return; // Не вызываем preventDefault, позволяем событию идти дальше
}
console.log('[MessageContextMenu] *** CONTEXTMENU EVENT FIRED ***');
console.log('[MessageContextMenu] Event details:', {
clientX: e.clientX,
clientY: e.clientY,
target: e.target.tagName,
messageId
});
e.preventDefault();
e.stopPropagation(); // Предотвращаем всплытие к обработчику на document
this.showMenu(messageElement, messageId, isOwn, e.clientX, e.clientY);
});
console.log('[MessageContextMenu] Contextmenu listener attached to message:', messageId);
// Мобилка: долгое нажатие
let touchStartX, touchStartY;
messageElement.addEventListener('touchstart', (e) => {
// Игнорируем долгое нажатие на кнопках реакций и контейнере реакций
if (e.target.closest('.reaction-button') || e.target.closest('.message-reactions')) {
console.log('[MessageContextMenu] Ignoring touchstart on reaction button/container');
return;
}
console.log('[MessageContextMenu] Touchstart event on message:', messageId);
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
this.longPressTimer = setTimeout(() => {
console.log('[MessageContextMenu] Long press triggered on message:', messageId);
// Вибрация для тактильной обратной связи
if (navigator.vibrate) {
navigator.vibrate(50);
}
this.showMenu(messageElement, messageId, isOwn, touchStartX, touchStartY);
}, this.longPressDuration);
}, { passive: true });
messageElement.addEventListener('touchmove', (e) => {
// Отменяем долгое нажатие если палец двигается
const touch = e.touches[0];
const moveX = Math.abs(touch.clientX - touchStartX);
const moveY = Math.abs(touch.clientY - touchStartY);
if (moveX > 10 || moveY > 10) {
console.log('[MessageContextMenu] Touch moved, cancelling long press');
clearTimeout(this.longPressTimer);
}
}, { passive: true });
messageElement.addEventListener('touchend', () => {
console.log('[MessageContextMenu] Touch ended');
clearTimeout(this.longPressTimer);
}, { passive: true });
messageElement.addEventListener('touchcancel', () => {
console.log('[MessageContextMenu] Touch cancelled');
clearTimeout(this.longPressTimer);
}, { passive: true });
console.log('[MessageContextMenu] All event listeners attached to message:', messageId);
}
/**
* Показать контекстное меню
*/
showMenu(messageElement, messageId, isOwn, x, y) {
console.log('[MessageContextMenu] showMenu called:', {
messageId,
isOwn,
x,
y,
messageElement: messageElement?.tagName
});
// Убрать подсветку с предыдущего сообщения (если было)
this.removeHighlight();
// Закрыть предыдущее меню
this.closeMenu();
// Создать меню
console.log('[MessageContextMenu] Creating menu...');
const menu = this.createMenu(messageElement, messageId, isOwn);
console.log('[MessageContextMenu] Menu created, appending to body');
document.body.appendChild(menu);
// Добавляем панель реакций отдельно
if (menu.reactionsPanel) {
document.body.appendChild(menu.reactionsPanel);
console.log('[MessageContextMenu] Reactions panel appended to body');
}
this.activeMenu = menu;
console.log('[MessageContextMenu] Menu appended to DOM');
// Добавить подсветку НОВОГО сообщения
console.log('[MessageContextMenu] Adding highlight to message');
messageElement.classList.add('message-highlighted');
this.highlightedMessage = messageElement; // Сохраняем ссылку
// Позиционировать меню и реакции
console.log('[MessageContextMenu] Positioning menu');
this.positionMenu(menu, x, y);
// Анимация появления
requestAnimationFrame(() => {
console.log('[MessageContextMenu] Adding show class for animation');
menu.classList.add('show');
if (menu.reactionsPanel) {
menu.reactionsPanel.classList.add('show');
}
});
}
/**
* Создать HTML меню
*/
createMenu(messageElement, messageId, isOwn) {
console.log('[MessageContextMenu] createMenu:', { messageId, isOwn });
const menu = document.createElement('div');
menu.className = 'message-context-menu';
menu.dataset.messageId = messageId;
console.log('[MessageContextMenu] Menu element created with class:', menu.className);
// Секция быстрых реакций (Telegram-style)
console.log('[MessageContextMenu] Creating reactions HTML with', this.emojis.length, 'emojis');
const reactionsHtml = this.emojis.map(emoji => `
`).join('') + `
`;
// Секция действий
const actionsHtml = `
`;
console.log('[MessageContextMenu] Setting innerHTML...');
menu.innerHTML = actionsHtml;
console.log('[MessageContextMenu] innerHTML set successfully');
// Создаём отдельный floating элемент для быстрых реакций (как в Telegram)
const reactionsPanel = document.createElement('div');
reactionsPanel.className = 'quick-reactions-panel';
reactionsPanel.innerHTML = `
`;
menu.reactionsPanel = reactionsPanel; // Сохраняем ссылку
console.log('[MessageContextMenu] Reactions panel created');
// Обработчик кликов на панели реакций
reactionsPanel.addEventListener('click', (e) => {
const btn = e.target.closest('[data-action]');
if (!btn) {
console.log('[MessageContextMenu] Click outside action button on reactions panel');
return;
}
e.preventDefault();
e.stopPropagation();
const action = btn.dataset.action;
const emoji = btn.dataset.emoji;
console.log('[MessageContextMenu] Reaction button clicked:', action, emoji);
// Если это кнопка "показать больше реакций" - раскрываем полный список
if (action === 'show-more-reactions') {
console.log('[MessageContextMenu] Show more reactions clicked - expanding');
this.expandReactionsList(reactionsPanel, messageElement, messageId);
return;
}
this.handleAction(action, messageElement, messageId, emoji);
this.removeHighlight();
this.closeMenu();
});
// Обработчики на основном меню
console.log('[MessageContextMenu] Attaching click handler to menu');
menu.addEventListener('click', (e) => {
const btn = e.target.closest('[data-action]');
if (!btn) {
console.log('[MessageContextMenu] Click outside action button');
return;
}
e.preventDefault();
e.stopPropagation();
const action = btn.dataset.action;
const emoji = btn.dataset.emoji;
console.log('[MessageContextMenu] Action button clicked:', action, emoji);
this.handleAction(action, messageElement, messageId, emoji);
this.removeHighlight(); // Убираем подсветку при выборе действия
this.closeMenu();
});
console.log('[MessageContextMenu] Menu fully constructed');
return menu;
}
/**
* Позиционировать меню на экране
*/
positionMenu(menu, x, y) {
console.log('[MessageContextMenu] positionMenu called:', { x, y });
const menuRect = menu.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const isMobile = viewportWidth <= 768;
console.log('[MessageContextMenu] Menu dimensions:', {
width: menuRect.width,
height: menuRect.height,
viewportWidth,
viewportHeight,
isMobile
});
let left, top;
// Единое позиционирование для всех устройств - рядом с курсором/касанием
left = x;
top = y;
// Не выходим за правый край
if (left + menuRect.width > viewportWidth) {
left = viewportWidth - menuRect.width - 10;
console.log('[MessageContextMenu] Adjusted left to prevent overflow:', left);
}
// Не выходим за левый край
if (left < 10) {
left = 10;
console.log('[MessageContextMenu] Adjusted left to prevent underflow:', left);
}
// Не выходим за нижний край
if (top + menuRect.height > viewportHeight) {
top = y - menuRect.height;
console.log('[MessageContextMenu] Adjusted top to prevent bottom overflow:', top);
}
// Не выходим за верхний край
if (top < 10) {
top = 10;
console.log('[MessageContextMenu] Adjusted top to prevent top overflow:', top);
}
console.log('[MessageContextMenu] Final position:', { left, top });
menu.style.left = `${left}px`;
menu.style.top = `${top}px`;
// Позиционируем панель реакций над меню (одинаково для всех устройств)
if (menu.reactionsPanel) {
const reactionsRect = menu.reactionsPanel.getBoundingClientRect();
const reactionsLeft = left;
const reactionsTop = top - reactionsRect.height - 12; // 12px gap
menu.reactionsPanel.style.left = `${reactionsLeft}px`;
menu.reactionsPanel.style.top = `${reactionsTop}px`;
}
}
/**
* Раскрыть список реакций (показать все доступные эмодзи)
*/
expandReactionsList(reactionsPanel, messageElement, messageId) {
const moreBtn = reactionsPanel.querySelector('.quick-reaction-btn-more');
const reactionsList = reactionsPanel.querySelector('.quick-reactions-list');
// Сохраняем текущую позицию и высоту
const currentTop = parseFloat(reactionsPanel.style.top);
const currentHeight = reactionsPanel.offsetHeight;
// Проверяем текущее состояние
const isExpanded = moreBtn.classList.contains('expanded');
if (isExpanded) {
// Сворачиваем - возвращаем только быстрые реакции
moreBtn.classList.remove('expanded');
const quickEmojis = this.emojis.map(emoji => `
`).join('') + `
`;
reactionsList.innerHTML = quickEmojis;
// Пересчитываем позицию после сворачивания (двигаем вниз)
requestAnimationFrame(() => {
const newHeight = reactionsPanel.offsetHeight;
const heightDiff = currentHeight - newHeight;
reactionsPanel.style.top = `${currentTop + heightDiff}px`;
});
} else {
// Раскрываем - показываем все эмодзи
moreBtn.classList.add('expanded');
const allEmojis = [
'👍', '👎', '❤️', '🔥', '😂', '😮', '😢', '😡',
'👏', '🙏', '💯', '🎉', '❓', '💪', '🤝', '👀',
'🤔', '😊', '😎', '🤗', '😴', '🥳', '😇', '🤩'
];
const expandedEmojis = allEmojis.map(emoji => `
`).join('') + `
`;
reactionsList.innerHTML = expandedEmojis;
// Пересчитываем позицию после раскрытия (двигаем вверх)
requestAnimationFrame(() => {
const newHeight = reactionsPanel.offsetHeight;
const heightDiff = newHeight - currentHeight;
reactionsPanel.style.top = `${currentTop - heightDiff}px`;
});
}
}
/**
* Обработать действие
*/
handleAction(action, messageElement, messageId, emoji) {
console.log('[ContextMenu] Action:', action, 'Message:', messageId);
switch (action) {
case 'react':
this.handleReact(messageId, emoji);
break;
case 'reply':
this.handleReply(messageElement);
break;
case 'edit':
this.handleEdit(messageElement);
break;
case 'delete':
this.handleDelete(messageId);
break;
case 'forward':
this.handleForward(messageId);
break;
case 'copy':
this.handleCopy(messageElement);
break;
case 'select':
this.handleSelect(messageElement, messageId);
break;
}
}
/**
* Добавить реакцию
*/
async handleReact(messageId, emoji) {
console.log('[ContextMenu] Adding reaction:', emoji, 'to message:', messageId);
try {
await this.onReactionSelect(messageId, emoji);
} catch (error) {
console.error('[ContextMenu] Failed to add reaction:', error);
this.showToast('Не удалось добавить реакцию');
}
}
/**
* Ответить на сообщение
*/
handleReply(messageElement) {
console.log('[MessageContextMenu] handleReply called');
const bubble = messageElement.querySelector('.bubble');
if (!bubble) {
console.warn('[MessageContextMenu] Bubble not found in message element');
return;
}
// Получаем данные сообщения
const messageText = bubble.textContent.trim();
const authorName = messageElement.querySelector('.small.text-secondary')?.textContent.split('·')[0]?.trim() || 'Пользователь';
const messageId = messageElement.dataset.messageId;
console.log('[MessageContextMenu] Reply data:', {
messageId,
authorName,
messageTextLength: messageText.length
});
// Используем chatFormManager если доступен
if (window.chatFormManager) {
console.log('[MessageContextMenu] Using chatFormManager.setModeToReply');
window.chatFormManager.setModeToReply(messageId, authorName, messageText);
} else {
console.warn('[MessageContextMenu] chatFormManager not available, using fallback');
// Фоллбэк на старую логику (на случай если модуль не загружен)
const textarea = document.querySelector('#id_content');
if (textarea) {
textarea.focus();
this.showToast(`Ответ на сообщение от ${authorName}`);
} else {
console.error('[MessageContextMenu] Textarea #id_content not found');
}
}
}
/**
* Редактировать сообщение
*/
async handleEdit(messageElement) {
console.log('[MessageContextMenu] handleEdit called');
const messageId = messageElement.dataset.messageId;
console.log('[MessageContextMenu] Message ID:', messageId);
// Получаем полные данные сообщения
let messageData = null;
// Приоритет 1: MessageStore (если сообщение уже загружено)
if (window.messageStore && typeof window.messageStore.getMessage === 'function') {
try {
messageData = window.messageStore.getMessage(messageId);
if (messageData) {
console.log('[MessageContextMenu] ✓ Message from Store:', {
id: messageData.id,
content: messageData.content?.substring(0, 50),
attachmentsCount: messageData.attachments?.length || 0
});
}
} catch (e) {
console.error('[MessageContextMenu] Store error:', e);
}
}
// Приоритет 2: ChatController V2
if (!messageData && window.chatController && typeof window.chatController.getMessage === 'function') {
try {
messageData = window.chatController.getMessage(messageId);
if (messageData) {
console.log('[MessageContextMenu] ✓ Message from ChatController');
}
} catch (e) {
console.error('[MessageContextMenu] ChatController error:', e);
}
}
// Fallback: Извлекаем из DOM (теряется Markdown и нет файлов)
if (!messageData) {
console.warn('[MessageContextMenu] Message not found in Store, using DOM fallback');
const bubble = messageElement.querySelector('.bubble');
const messageContent = messageElement.querySelector('.message-content');
console.log('[MessageContextMenu] DOM elements found:', {
hasBubble: !!bubble,
hasMessageContent: !!messageContent,
bubbleHTML: bubble?.innerHTML.substring(0, 200),
messageContentHTML: messageContent?.innerHTML.substring(0, 200)
});
if (!bubble && !messageContent) {
console.error('[MessageContextMenu] Neither .bubble nor .message-content found!');
return;
}
// Извлекаем текст - приоритет .message-content, fallback на .bubble
const sourceElement = messageContent || bubble;
let messageText = '';
// Более аккуратное извлечение текста
const extractText = (element) => {
let text = '';
element.childNodes.forEach(node => {
if (node.nodeType === Node.TEXT_NODE) {
text += node.textContent;
} else if (node.nodeName === 'BR') {
text += '\n';
} else if (node.nodeType === Node.ELEMENT_NODE) {
// Рекурсивно извлекаем текст из вложенных элементов
// НО игнорируем системные элементы
if (!node.classList.contains('edited-indicator') &&
!node.classList.contains('message-time') &&
!node.classList.contains('message-attachments') &&
!node.classList.contains('message-reactions')) {
text += extractText(node);
}
}
});
return text;
};
messageText = extractText(sourceElement).trim();
console.log('[MessageContextMenu] Extracted text:', {
length: messageText.length,
preview: messageText.substring(0, 100)
});
// Извлекаем вложения из DOM
const attachments = [];
const attachmentsContainer = messageElement.querySelector('.message-attachments');
console.log('[MessageContextMenu] DOM: Searching for attachments...', {
hasContainer: !!attachmentsContainer,
messageElement: messageElement
});
if (attachmentsContainer) {
// Ищем все типы вложений: .attachment-link (файлы) и .attachment-item (медиа)
const attachmentElements = attachmentsContainer.querySelectorAll('.attachment-link, .attachment-item');
console.log('[MessageContextMenu] DOM: Found attachments:', attachmentElements.length);
attachmentElements.forEach((element, index) => {
let fileUrl, fileName, fileSize = '';
if (element.classList.contains('attachment-link')) {
// Обычные файлы (документы, аудио)
fileUrl = element.getAttribute('href');
fileName = element.querySelector('.attachment-name')?.textContent || `Файл ${index + 1}`;
fileSize = element.querySelector('.attachment-size')?.textContent || '';
} else if (element.classList.contains('attachment-item')) {
// Медиа файлы (изображения, видео)
const linkElement = element.tagName === 'A' ? element : element.closest('a');
const imgElement = element.querySelector('img');
const videoElement = element.querySelector('video');
if (linkElement) {
fileUrl = linkElement.getAttribute('href');
} else if (imgElement) {
fileUrl = imgElement.getAttribute('src');
} else if (videoElement) {
fileUrl = videoElement.getAttribute('src');
}
if (imgElement) {
fileName = imgElement.getAttribute('alt') || `Изображение ${index + 1}`;
} else if (videoElement) {
fileName = `Видео ${index + 1}`;
} else {
fileName = `Медиа ${index + 1}`;
}
}
if (!fileUrl) {
console.warn('[MessageContextMenu] Skipping attachment without URL:', element);
return;
}
// Извлекаем ID из data-атрибута (приоритет) или из URL (fallback)
let attachmentId = element.getAttribute('data-attachment-id');
if (!attachmentId) {
// Fallback: пробуем извлечь из URL (может не сработать)
console.warn('[MessageContextMenu] No data-attachment-id, trying URL extraction');
attachmentId = `url-${fileUrl.split('/').pop().split('.')[0]}-${index}`;
}
console.log('[MessageContextMenu] ID extraction:', { fileUrl, dataAttrId: element.getAttribute('data-attachment-id'), attachmentId });
const attachment = {
id: attachmentId,
filename: fileName,
file_name: fileName,
file_url: fileUrl,
url: fileUrl,
size_str: fileSize
};
attachments.push(attachment);
console.log('[MessageContextMenu] ✓ Extracted attachment from DOM:', {
index,
type: element.classList.contains('attachment-link') ? 'file' : 'media',
fileName,
fileUrl,
attachmentId,
fullObject: attachment
});
});
}
messageData = {
id: messageId,
content: messageText,
attachments: attachments
};
console.log('[MessageContextMenu] Extracted from DOM:', {
textLength: messageText.length,
attachmentsCount: attachments.length
});
}
console.log('[MessageContextMenu] Final message data:', {
id: messageData.id,
contentLength: messageData.content?.length || 0,
hasAttachments: !!messageData.attachments?.length,
attachmentsCount: messageData.attachments?.length || 0,
attachments: messageData.attachments // Полный список для отладки
});
// Используем chatFormManager если доступен
if (window.chatFormManager && typeof window.chatFormManager.setModeToEdit === 'function') {
console.log('[MessageContextMenu] Calling chatFormManager.setModeToEdit with:', {
id: messageData.id,
contentLength: messageData.content?.length || 0,
attachmentsCount: messageData.attachments?.length || 0,
attachments: messageData.attachments
});
window.chatFormManager.setModeToEdit(
messageData.id,
messageData.content || '',
messageData.attachments || []
);
} else {
console.error('[MessageContextMenu] chatFormManager not available or setModeToEdit not a function');
console.log('[MessageContextMenu] window.chatFormManager:', window.chatFormManager);
}
}
/**
* Удалить сообщение
*/
async handleDelete(messageId) {
console.log('[MessageContextMenu] handleDelete called:', messageId);
if (!confirm('Удалить это сообщение?')) {
console.log('[MessageContextMenu] Delete cancelled by user');
return;
}
try {
const csrfToken = this.getCsrfToken();
const url = `/api/v1/communications/messages/${messageId}/`;
console.log('[MessageContextMenu] Sending DELETE request to:', url);
console.log('[MessageContextMenu] CSRF token:', csrfToken ? 'present' : 'missing');
const response = await fetch(url, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
}
});
console.log('[MessageContextMenu] Response status:', response.status);
console.log('[MessageContextMenu] Response ok:', response.ok);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
// Статус 204 = No Content, не парсим JSON
if (response.status === 204) {
console.log('[MessageContextMenu] Delete successful (204)');
console.log('[MessageContextMenu] NOTE: DOM will be updated via WebSocket');
// НЕ удаляем из DOM здесь - это сделает WebSocket handler
// Так все пользователи увидят удаление в реальном времени
this.showToast('Сообщение удалено');
console.log('[MessageContextMenu] Toast shown');
return;
}
const data = await response.json();
console.log('[MessageContextMenu] Response data:', data);
if (data.ok) {
console.log('[MessageContextMenu] Delete successful');
console.log('[MessageContextMenu] NOTE: DOM will be updated via WebSocket');
// НЕ удаляем из DOM здесь - это сделает WebSocket handler
// Так все пользователи увидят удаление в реальном времени
this.showToast('Сообщение удалено');
console.log('[MessageContextMenu] Toast shown');
} else {
throw new Error(data.error || 'Failed to delete message');
}
} catch (error) {
console.error('[MessageContextMenu] Failed to delete message:', error);
this.showToast('Не удалось удалить сообщение');
}
}
/**
* Переслать сообщение
*/
handleForward(messageId) {
console.log('[ContextMenu] Forward message:', messageId);
// Отправляем событие для активации режима пересылки одного сообщения
window.dispatchEvent(new CustomEvent('message-forward-single', {
detail: { messageId }
}));
this.showToast('Выберите чат для пересылки');
}
/**
* Копировать текст сообщения
*/
async handleCopy(messageElement) {
console.log('[ContextMenu] Copy message text');
const bubble = messageElement.querySelector('.bubble');
if (!bubble) {
console.warn('[ContextMenu] Bubble not found');
return;
}
// Получаем только содержимое сообщения (без автора, времени, реакций)
const messageContent = bubble.querySelector('.message-content');
if (!messageContent) {
console.warn('[ContextMenu] Message content not found');
return;
}
// Копируем только чистый текст сообщения
const text = messageContent.textContent
.replace(/\(ред\.\)/, '') // Убираем метку редактирования
.trim();
try {
await navigator.clipboard.writeText(text);
this.showToast('Текст скопирован');
} catch (err) {
console.error('[ContextMenu] Failed to copy text:', err);
this.showToast('Не удалось скопировать');
}
}
/**
* Выбрать сообщение
*/
handleSelect(messageElement, messageId) {
console.log('[ContextMenu] Select message:', messageId);
// Отправляем событие для активации режима выделения
window.dispatchEvent(new CustomEvent('message-context-menu', {
detail: {
action: 'select',
messageId: messageId,
messageElement: messageElement
}
}));
}
/**
* Получить CSRF токен
*/
getCsrfToken() {
const cookie = document.cookie
.split('; ')
.find(row => row.startsWith('csrftoken='));
return cookie ? cookie.split('=')[1] : '';
}
/**
* Экранировать HTML
*/
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Показать временное уведомление
*/
showToast(message) {
console.log('[MessageContextMenu] showToast:', message);
const toast = document.createElement('div');
toast.className = 'context-menu-toast';
toast.textContent = message;
console.log('[MessageContextMenu] Toast created, appending to body');
document.body.appendChild(toast);
requestAnimationFrame(() => {
console.log('[MessageContextMenu] Adding show class to toast');
toast.classList.add('show');
});
setTimeout(() => {
console.log('[MessageContextMenu] Removing show class from toast');
toast.classList.remove('show');
setTimeout(() => {
console.log('[MessageContextMenu] Removing toast from DOM');
toast.remove();
}, 300);
}, 2000);
}
/**
* Убрать подсветку сообщения
*/
removeHighlight() {
if (this.highlightedMessage) {
console.log('[MessageContextMenu] Removing highlight from message:', this.highlightedMessage.dataset.messageId);
this.highlightedMessage.classList.remove('message-highlighted');
this.highlightedMessage = null;
console.log('[MessageContextMenu] Highlight removed');
} else {
console.log('[MessageContextMenu] No highlighted message to remove');
}
}
/**
* Закрыть меню
*/
closeMenu() {
if (!this.activeMenu) {
console.log('[MessageContextMenu] closeMenu: no active menu');
return;
}
console.log('[MessageContextMenu] Closing menu');
// Сохраняем ссылку на закрываемое меню и панель реакций
const menuToClose = this.activeMenu;
const reactionsToClose = menuToClose.reactionsPanel;
// Сразу обнуляем activeMenu (важно для быстрых кликов!)
this.activeMenu = null;
// Анимация закрытия
console.log('[MessageContextMenu] Removing show class for fade out');
menuToClose.classList.remove('show');
if (reactionsToClose) {
reactionsToClose.classList.remove('show');
}
setTimeout(() => {
console.log('[MessageContextMenu] Removing menu from DOM');
menuToClose.remove();
if (reactionsToClose) {
reactionsToClose.remove();
}
}, 200);
}
}
export default MessageContextMenu;