- Исправлен PollSerializer для создания опросов с опциями - Добавлено поле read_only_fields для корректной валидации - Реализована отправка WebSocket уведомлений при создании опроса - Исправлен импорт serialize_message из communications.serialization - Добавлен обработчик poll_update в UserConsumer для WebSocket событий - Исправлен эндпоинт /results/ для возврата user_voted_option_ids - Добавлено поле voters в PollOptionSerializer для списка проголосовавших - Реализована инициализация poll виджетов после рендера сообщений - Добавлено игнорирование poll_update в notification-manager - Исправлены стили для request list badges (height, word-break, flex) - Восстановлены стили team-wheel (.wheel-focus, .scroll-arc) из git истории - Реализовано отображение forwarded message info в messageRendererV2 - Множественные улучшения UI и UX для опросов Fixes: создание опросов, голосование, отображение результатов, список проголосовавших
308 lines
15 KiB
JavaScript
308 lines
15 KiB
JavaScript
/**
|
||
* Enhanced Chat Detail - только UI компоненты (реакции, контекстное меню, выделение, голосования)
|
||
* WebSocket управление вынесено в chatWebSocket.js (base.html)
|
||
*/
|
||
import MessageReactions from './components/messageReactions.js';
|
||
import MessageContextMenu from './components/messageContextMenu.js';
|
||
import MessageSelection from './components/messageSelection.js';
|
||
import ChatPoll from './components/chatPoll.js';
|
||
import { initMessageEditing } from './components/messageEditing.js';
|
||
import { initChatFormManager } from './components/chatFormManager.js';
|
||
import reactionsConfig from './config/reactionsConfig.js';
|
||
|
||
console.log('[chat-detail-enhanced] Script loaded');
|
||
|
||
// Защита от повторной инициализации
|
||
if (window.__CHAT_DETAIL_ENHANCED_INITIALIZED__) {
|
||
console.warn('chat-detail-enhanced.js: already initialized, skipping');
|
||
} else {
|
||
console.log('[chat-detail-enhanced] Starting initialization...');
|
||
window.__CHAT_DETAIL_ENHANCED_INITIALIZED__ = true;
|
||
|
||
// Главная функция инициализации
|
||
async function initChatDetail() {
|
||
'use strict';
|
||
|
||
const chatScroll = document.getElementById('chatScroll');
|
||
console.log('[chat-detail-enhanced] chatScroll element:', chatScroll);
|
||
if (!chatScroll) return;
|
||
|
||
const chatId = chatScroll.dataset.chatId;
|
||
const meId = parseInt(chatScroll.dataset.meId);
|
||
console.log('[chat-detail-enhanced] chatId:', chatId, 'meId:', meId);
|
||
|
||
// Загружаем доступные реакции из БД
|
||
console.log('[chat-detail-enhanced] Loading available reactions from API...');
|
||
await reactionsConfig.load();
|
||
const allEmojis = reactionsConfig.getEmojis();
|
||
// Берем только первые 5 эмодзи для быстрых реакций
|
||
const availableEmojis = allEmojis.slice(0, 5);
|
||
console.log('[chat-detail-enhanced] ✓ Available emojis (quick):', availableEmojis);
|
||
console.log('[chat-detail-enhanced] ✓ All emojis:', allEmojis);
|
||
|
||
// Инициализируем систему реакций
|
||
const reactions = new MessageReactions();
|
||
console.log('[chat-detail-enhanced] MessageReactions initialized');
|
||
|
||
// Делаем доступным глобально
|
||
window.reactions = reactions;
|
||
|
||
// Инициализируем обработку редактирования сообщений
|
||
initMessageEditing();
|
||
console.log('[chat-detail-enhanced] Message editing initialized');
|
||
|
||
// Инициализируем менеджер формы (для редактирования/ответов)
|
||
const formManager = initChatFormManager({
|
||
chatId: chatId,
|
||
formId: 'chatForm',
|
||
textareaId: 'id_content'
|
||
});
|
||
|
||
if (formManager) {
|
||
console.log('[chat-detail-enhanced] ChatFormManager initialized');
|
||
} else {
|
||
console.warn('[chat-detail-enhanced] ChatFormManager init failed - form elements not found');
|
||
}
|
||
|
||
// Инициализируем систему выделения сообщений
|
||
console.log('[chat-detail-enhanced] Initializing MessageSelection...');
|
||
const messageSelection = new MessageSelection({
|
||
chatId: chatId,
|
||
scrollContainerId: 'chatScroll',
|
||
currentUserId: String(meId),
|
||
onForward: (messageIds, targetChatId) => {
|
||
console.log('Messages forwarded:', messageIds, 'to chat:', targetChatId);
|
||
}
|
||
});
|
||
console.log('[chat-detail-enhanced] MessageSelection initialized');
|
||
|
||
// Инициализируем систему голосований
|
||
const chatPoll = new ChatPoll({
|
||
chatId: chatId
|
||
});
|
||
console.log('[chat-detail-enhanced] ChatPoll initialized');
|
||
|
||
// Делаем доступным глобально для использования в других модулей
|
||
window.chatPoll = chatPoll;
|
||
|
||
// ==================== ОБРАБОТЧИК КЛИКОВ ПО ПРЕВЬЮ ОТВЕТОВ ====================
|
||
|
||
/**
|
||
* Делегированный обработчик для всех .message-reply-preview
|
||
* Работает как для серверных, так и для динамических сообщений
|
||
*/
|
||
chatScroll.addEventListener('click', (e) => {
|
||
const replyPreview = e.target.closest('.message-reply-preview');
|
||
if (!replyPreview) return;
|
||
|
||
const replyToId = replyPreview.dataset.replyToId;
|
||
if (!replyToId) return;
|
||
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
// Ищем целевое сообщение
|
||
const targetMsg = document.querySelector(`[data-message-id="${replyToId}"]`);
|
||
if (!targetMsg) return;
|
||
|
||
// Скроллим к сообщению
|
||
targetMsg.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
|
||
// Добавляем анимацию подсветки к .msg
|
||
targetMsg.classList.remove('highlight-flash');
|
||
setTimeout(() => {
|
||
targetMsg.classList.add('highlight-flash');
|
||
setTimeout(() => targetMsg.classList.remove('highlight-flash'), 1500);
|
||
}, 10);
|
||
});
|
||
|
||
// ==================== WebSocket ОБРАБОТЧИКИ РЕАКЦИЙ ====================
|
||
|
||
// Обработка добавления реакции через WebSocket
|
||
window.addEventListener('chat:reaction-added', (event) => {
|
||
console.log('[chat-detail-enhanced] WebSocket: reaction-added', event.detail);
|
||
const data = event.detail;
|
||
const messageId = data.message_id || data.messageId;
|
||
|
||
console.log('[chat-detail-enhanced] Looking for message ID:', messageId);
|
||
console.log('[chat-detail-enhanced] Reactions summary:', data.reactions_summary);
|
||
|
||
const messageElements = document.querySelectorAll(`[data-message-id="${messageId}"]`);
|
||
console.log('[chat-detail-enhanced] Found message elements:', messageElements.length);
|
||
|
||
if (messageElements.length && data.reactions_summary) {
|
||
messageElements.forEach(messageElement => {
|
||
// Используем централизованный MessageReactions для обновления
|
||
reactions.updateMessageReactions(messageElement, data.reactions_summary, meId);
|
||
});
|
||
} else {
|
||
if (!data.reactions_summary) {
|
||
console.warn('[chat-detail-enhanced] ✗ No reactions_summary in WebSocket data');
|
||
}
|
||
if (!messageElements.length) {
|
||
console.warn('[chat-detail-enhanced] ✗ Message element not found for ID:', messageId);
|
||
}
|
||
}
|
||
});
|
||
|
||
// Обработка удаления реакции через WebSocket
|
||
// Обработка удаления реакции через WebSocket
|
||
window.addEventListener('chat:reaction-removed', (event) => {
|
||
console.log('[chat-detail-enhanced] WebSocket: reaction-removed', event.detail);
|
||
const data = event.detail;
|
||
const messageId = data.message_id || data.messageId;
|
||
|
||
console.log('[chat-detail-enhanced] Looking for message ID:', messageId);
|
||
console.log('[chat-detail-enhanced] Reactions summary:', data.reactions_summary);
|
||
|
||
const messageElements = document.querySelectorAll(`[data-message-id="${messageId}"]`);
|
||
console.log('[chat-detail-enhanced] Found message elements:', messageElements.length);
|
||
|
||
if (messageElements.length && data.reactions_summary !== undefined) {
|
||
messageElements.forEach(messageElement => {
|
||
// Используем централизованный MessageReactions для обновления
|
||
reactions.updateMessageReactions(messageElement, data.reactions_summary, meId);
|
||
});
|
||
} else {
|
||
if (data.reactions_summary === undefined) {
|
||
console.warn('[chat-detail-enhanced] ✗ No reactions_summary in WebSocket data');
|
||
}
|
||
if (!messageElements.length) {
|
||
console.warn('[chat-detail-enhanced] ✗ Message element not found for ID:', messageId);
|
||
}
|
||
}
|
||
});
|
||
|
||
// ==================== ИНИЦИАЛИЗАЦИЯ КОНТЕКСТНОГО МЕНЮ ====================
|
||
|
||
// Инициализируем контекстное меню с динамическими эмодзи из БД
|
||
const contextMenu = new MessageContextMenu({
|
||
currentUserId: meId,
|
||
emojis: availableEmojis, // Используем загруженные из БД
|
||
onReactionSelect: async (messageId, emoji) => {
|
||
await reactions.addReaction(messageId, emoji);
|
||
}
|
||
});
|
||
|
||
// ==================== Глобальные функции для совместимости ====================
|
||
|
||
// Эти функции нужны для работы со старым HTML (inline onclick)
|
||
window.addReaction = function (messageId, emoji) {
|
||
reactions.addReaction(messageId, emoji);
|
||
};
|
||
|
||
window.toggleReaction = function (messageId, emoji) {
|
||
const msgEl = document.querySelector(`[data-message-id="${messageId}"]`);
|
||
const reactionBtn = msgEl?.querySelector(`.reaction-item[data-emoji="${emoji}"]`);
|
||
|
||
if (reactionBtn?.classList.contains('active')) {
|
||
reactions.removeReaction(messageId, emoji);
|
||
} else {
|
||
reactions.addReaction(messageId, emoji);
|
||
}
|
||
};
|
||
|
||
// ==================== Инициализация существующих сообщений ====================
|
||
|
||
// Инициализация реакций для существующих сообщений
|
||
function initMessageReactions() {
|
||
const messages = document.querySelectorAll('[data-message-id]');
|
||
console.log('[chat-detail-enhanced] Initializing reactions for', messages.length, 'messages');
|
||
|
||
messages.forEach(messageElement => {
|
||
const messageId = messageElement.dataset.messageId;
|
||
|
||
// Инициализировать обработчики реакций если есть контейнер
|
||
const reactionsContainer = messageElement.querySelector('.message-reactions');
|
||
if (reactionsContainer) {
|
||
console.log('[chat-detail-enhanced] Initializing reactions handlers for message:', messageId);
|
||
reactions.initMessageReactions(messageElement, messageId, meId);
|
||
}
|
||
|
||
// Подключить контекстное меню к сообщению
|
||
if (!messageElement.dataset.contextMenuAttached) {
|
||
contextMenu.attachToMessage(messageElement);
|
||
messageElement.dataset.contextMenuAttached = 'true';
|
||
messageElement.classList.add('message-context-menu-enabled');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Наблюдатель за новыми сообщениями
|
||
const messageObserver = new MutationObserver((mutations) => {
|
||
mutations.forEach((mutation) => {
|
||
mutation.addedNodes.forEach((node) => {
|
||
if (node.nodeType === 1 && node.dataset && node.dataset.messageId) {
|
||
console.log('[chat-detail-enhanced] New message detected:', node.dataset.messageId);
|
||
|
||
// Инициализировать обработчики реакций если есть контейнер
|
||
const reactionsContainer = node.querySelector('.message-reactions');
|
||
if (reactionsContainer) {
|
||
console.log('[chat-detail-enhanced] Initializing reactions handlers for new message');
|
||
reactions.initMessageReactions(node, node.dataset.messageId, meId);
|
||
}
|
||
|
||
// Подключить контекстное меню
|
||
if (!node.dataset.contextMenuAttached) {
|
||
contextMenu.attachToMessage(node);
|
||
node.dataset.contextMenuAttached = 'true';
|
||
node.classList.add('message-context-menu-enabled');
|
||
}
|
||
}
|
||
});
|
||
});
|
||
});
|
||
|
||
messageObserver.observe(chatScroll, {
|
||
childList: true,
|
||
subtree: true
|
||
});
|
||
|
||
// ==================== Init ====================
|
||
|
||
// Делегированный обработчик кликов на реакции - используем MessageReactions
|
||
document.addEventListener('click', async (e) => {
|
||
const reactionBtn = e.target.closest('.reaction-button');
|
||
if (reactionBtn && !reactionBtn.classList.contains('add-reaction')) {
|
||
e.preventDefault();
|
||
|
||
const messageEl = reactionBtn.closest('[data-message-id]');
|
||
if (!messageEl) return;
|
||
|
||
const messageId = messageEl.dataset.messageId;
|
||
const emoji = reactionBtn.dataset.emoji;
|
||
const isActive = reactionBtn.classList.contains('active');
|
||
|
||
try {
|
||
if (isActive) {
|
||
await reactions.removeReaction(messageId);
|
||
} else {
|
||
await reactions.addReaction(messageId, emoji);
|
||
}
|
||
} catch (error) {
|
||
console.error('[chat-detail-enhanced] Reaction error:', error);
|
||
}
|
||
}
|
||
});
|
||
|
||
initMessageReactions();
|
||
|
||
// Обработка обновлений голосований через WebSocket
|
||
window.addEventListener('chat:poll-update', (event) => {
|
||
const { poll_id } = event.detail;
|
||
if (window.chatPoll) {
|
||
// Всегда делаем refreshPoll для получения персонализированных данных
|
||
// (user_voted_option_ids не передаётся через WebSocket, т.к. это общий канал)
|
||
console.log('[chat-detail-enhanced] Poll update received, refreshing poll_id=%s', poll_id);
|
||
window.chatPoll.refreshPoll(poll_id);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Запускаем инициализацию
|
||
initChatDetail().catch(error => {
|
||
console.error('[chat-detail-enhanced] Initialization failed:', error);
|
||
});
|
||
|
||
} // end if (__CHAT_DETAIL_ENHANCED_INITIALIZED__)
|