Corp/backend/static/js/chat-detail-enhanced.js
igor_nadein da7bf54c6c feat: Реализация полной функциональности опросов в чатах
- Исправлен 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: создание опросов, голосование, отображение результатов, список проголосовавших
2026-01-22 01:33:42 +03:00

308 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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__)