refactor: удалены test/example файлы и deprecated templates
- Deleted chatHistoryLoader.js (@deprecated) - Deleted chatControllerUsage.js (example) - Deleted chatTests.js (manual test, 1732 lines) - Deleted test_message_editing.html - Deleted test_message_renderer.html - Deleted chat_detail_scripts.html (неиспользуемый template)
This commit is contained in:
parent
697f8c6d37
commit
2a012c297f
@ -1,274 +0,0 @@
|
||||
/**
|
||||
* @fileoverview Chat History Loader - DEPRECATED
|
||||
* @module components/chatHistoryLoader
|
||||
*
|
||||
* @deprecated Используйте ChatControllerV2 вместо этого модуля.
|
||||
* Этот модуль сохранен для обратной совместимости.
|
||||
*
|
||||
* МИГРАЦИЯ:
|
||||
* - Вместо initChatHistoryLoader() используйте ChatControllerV2
|
||||
* - Вместо loadMore() используйте chatController.loadMoreHistory()
|
||||
* - Вся логика загрузки теперь в MessageLoaderV2 и ScrollManagerV2
|
||||
*
|
||||
* @see controllers/chatControllerV2.js
|
||||
* @see loaders/messageLoaderV2.js
|
||||
* @see managers/scrollManagerV2.js
|
||||
*/
|
||||
|
||||
import {
|
||||
createMessageElement,
|
||||
formatDay,
|
||||
toTimestamp
|
||||
} from './chatMessageTemplates.js';
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
scrollSelector: '#chatScroll',
|
||||
loaderSelector: '[data-history-loader]',
|
||||
triggerThreshold: 140,
|
||||
pageSize: 30
|
||||
};
|
||||
|
||||
function parseBool(value) {
|
||||
if (typeof value === 'boolean') return value;
|
||||
return value === '1' || value === 'true';
|
||||
}
|
||||
|
||||
function getAvatarMapSnapshot() {
|
||||
if (window?.chatAvatarMap?.getAll) {
|
||||
return window.chatAvatarMap.getAll();
|
||||
}
|
||||
if (window?.__CHAT_AVATARS__) {
|
||||
return { ...window.__CHAT_AVATARS__ };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
function updateOldestDataset(scrollEl) {
|
||||
const firstMsg = scrollEl.querySelector('.msg[data-id]');
|
||||
if (!firstMsg) {
|
||||
scrollEl.dataset.oldestId = '';
|
||||
scrollEl.dataset.oldestTs = '';
|
||||
return null;
|
||||
}
|
||||
scrollEl.dataset.oldestId = String(firstMsg.dataset.id || '');
|
||||
scrollEl.dataset.oldestTs = String(firstMsg.dataset.ts || '');
|
||||
return firstMsg;
|
||||
}
|
||||
|
||||
function ensureLoader(scrollEl, selector) {
|
||||
let loader = scrollEl.querySelector(selector);
|
||||
if (!loader) {
|
||||
loader = document.createElement('div');
|
||||
loader.className = 'history-loader d-none';
|
||||
loader.setAttribute('data-history-loader', '1');
|
||||
loader.innerHTML = `
|
||||
<div class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></div>
|
||||
<span class="small">Загружаем ранние сообщения…</span>
|
||||
`;
|
||||
scrollEl.prepend(loader);
|
||||
}
|
||||
return loader;
|
||||
}
|
||||
|
||||
export function initChatHistoryLoader(options = {}) {
|
||||
const config = { ...DEFAULT_CONFIG, ...options };
|
||||
const scrollEl = document.querySelector(config.scrollSelector);
|
||||
if (!scrollEl) return null;
|
||||
|
||||
const fetchUrl = scrollEl.dataset.fetchUrl;
|
||||
if (!fetchUrl) return null;
|
||||
|
||||
const loaderEl = ensureLoader(scrollEl, config.loaderSelector);
|
||||
const meId = Number(scrollEl.dataset.meId || 0);
|
||||
const profileUrl = scrollEl.dataset.profileUrl || '/employees/profile/';
|
||||
const detailUrlTemplate = scrollEl.dataset.detailUrlTemplate || '/employees/0/';
|
||||
const avatarMap = getAvatarMapSnapshot();
|
||||
const messageRenderer = options.messageRenderer || null; // РЕФАКТОРИНГ: Принимаем MessageRenderer
|
||||
|
||||
const state = {
|
||||
loading: false,
|
||||
hasMore: parseBool(scrollEl.dataset.hasMore || false),
|
||||
oldestId: (() => {
|
||||
const raw = scrollEl.dataset.oldestId;
|
||||
if (!raw || raw === 'None') return null;
|
||||
return raw;
|
||||
})(),
|
||||
fetchUrl,
|
||||
limit: Number(scrollEl.dataset.pageSize || config.pageSize) || config.pageSize
|
||||
};
|
||||
|
||||
function showLoader() {
|
||||
loaderEl.classList.remove('d-none');
|
||||
}
|
||||
|
||||
function hideLoader() {
|
||||
loaderEl.classList.add('d-none');
|
||||
}
|
||||
|
||||
function dedupeDayDividers(scrollEl) {
|
||||
const dividerEls = scrollEl.querySelectorAll('.day-divider');
|
||||
let lastLabel = null;
|
||||
dividerEls.forEach((divider) => {
|
||||
const label = divider.textContent.trim();
|
||||
if (label === lastLabel) {
|
||||
divider.remove();
|
||||
} else {
|
||||
lastLabel = label;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function prependMessages(messages = [], ctx) {
|
||||
if (!messages.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
scrollEl,
|
||||
loaderEl,
|
||||
meId,
|
||||
profileUrl,
|
||||
detailUrlTemplate,
|
||||
avatarMap,
|
||||
messageRenderer,
|
||||
state
|
||||
} = ctx;
|
||||
|
||||
const prevHeight = scrollEl.scrollHeight;
|
||||
const fragment = document.createDocumentFragment();
|
||||
let prevDay = null;
|
||||
|
||||
// РЕФАКТОРИНГ: Создаем day-dividers вручную с правильным стилем
|
||||
// Используем messageRenderer.formatDay() для единообразия
|
||||
messages.forEach((msg) => {
|
||||
// Определяем день сообщения
|
||||
const ts = toTimestamp(msg);
|
||||
const msgDate = new Date(ts);
|
||||
const day = messageRenderer ? messageRenderer.formatDay(msgDate) : formatDay(ts);
|
||||
|
||||
// Создаем day-divider если день изменился
|
||||
if (day !== prevDay) {
|
||||
// Создаем divider с ПРАВИЛЬНЫМ стилем (как в messageRenderer)
|
||||
const dividerEl = document.createElement('div');
|
||||
dividerEl.className = 'day-divider text-center small text-muted my-3';
|
||||
dividerEl.innerHTML = `<span class="px-3 py-1 rounded-pill bg-light">${day}</span>`;
|
||||
fragment.appendChild(dividerEl);
|
||||
}
|
||||
prevDay = day;
|
||||
|
||||
// Создаем элемент сообщения
|
||||
let element;
|
||||
if (messageRenderer) {
|
||||
element = messageRenderer.createMessageElement(msg, false);
|
||||
} else {
|
||||
// Fallback для обратной совместимости
|
||||
element = createMessageElement(msg, {
|
||||
meId,
|
||||
profileUrl,
|
||||
detailUrlTemplate,
|
||||
avatarMap
|
||||
});
|
||||
}
|
||||
|
||||
element.classList.remove('message-pending', 'message-pending--resolved');
|
||||
fragment.appendChild(element);
|
||||
});
|
||||
|
||||
const anchor = loaderEl.nextSibling || scrollEl.firstChild;
|
||||
scrollEl.insertBefore(fragment, anchor);
|
||||
|
||||
const delta = scrollEl.scrollHeight - prevHeight;
|
||||
scrollEl.scrollTop += delta;
|
||||
|
||||
dedupeDayDividers(scrollEl);
|
||||
|
||||
const firstMsg = updateOldestDataset(scrollEl);
|
||||
if (firstMsg) {
|
||||
state.oldestId = firstMsg.dataset.id;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (state.loading || !state.hasMore || !state.oldestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.loading = true;
|
||||
showLoader();
|
||||
|
||||
try {
|
||||
const url = new URL(state.fetchUrl, window.location.origin);
|
||||
url.searchParams.set('limit', String(state.limit));
|
||||
url.searchParams.set('before_id', state.oldestId);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
},
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
const payload = await response.json();
|
||||
if (!response.ok || !payload.ok) {
|
||||
throw new Error(payload.error || 'Не удалось загрузить сообщения');
|
||||
}
|
||||
|
||||
if (Array.isArray(payload.messages) && payload.messages.length) {
|
||||
prependMessages(payload.messages, {
|
||||
scrollEl,
|
||||
loaderEl,
|
||||
meId,
|
||||
profileUrl,
|
||||
detailUrlTemplate,
|
||||
avatarMap,
|
||||
messageRenderer,
|
||||
state
|
||||
});
|
||||
state.oldestId = String(payload.messages[0].id);
|
||||
} else if (payload.next_before_id) {
|
||||
state.oldestId = String(payload.next_before_id);
|
||||
} else {
|
||||
state.oldestId = null;
|
||||
}
|
||||
|
||||
state.hasMore = Boolean(payload.has_more);
|
||||
scrollEl.dataset.hasMore = state.hasMore ? '1' : '0';
|
||||
|
||||
} catch (error) {
|
||||
console.error('ChatHistoryLoader: history request failed', error);
|
||||
} finally {
|
||||
state.loading = false;
|
||||
hideLoader();
|
||||
}
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
if (!state.hasMore || state.loading) {
|
||||
return;
|
||||
}
|
||||
if (scrollEl.scrollTop <= config.triggerThreshold) {
|
||||
loadMore();
|
||||
}
|
||||
}
|
||||
|
||||
scrollEl.addEventListener('scroll', handleScroll, { passive: true });
|
||||
|
||||
if (!state.hasMore) {
|
||||
hideLoader();
|
||||
} else if (scrollEl.scrollHeight <= scrollEl.clientHeight + config.triggerThreshold) {
|
||||
// Если сообщений мало и страница «короткая», сразу догружаем ещё
|
||||
loadMore();
|
||||
}
|
||||
|
||||
return {
|
||||
loadMore,
|
||||
hasMore: () => state.hasMore
|
||||
};
|
||||
}
|
||||
|
||||
// УДАЛЕНО: Авто-инициализация теперь происходит через chat_detail_scripts.html
|
||||
// if (document.readyState === 'loading') {
|
||||
// document.addEventListener('DOMContentLoaded', () => initChatHistoryLoader());
|
||||
// } else {
|
||||
// initChatHistoryLoader();
|
||||
// }
|
||||
@ -1,220 +0,0 @@
|
||||
/**
|
||||
* @fileoverview Пример использования новой архитектуры
|
||||
*
|
||||
* Демонстрирует как использовать ChatController
|
||||
* вместо старых разрозненных модулей
|
||||
*/
|
||||
|
||||
import { ChatController } from './controllers/chatController.js';
|
||||
|
||||
// ==================== СТАРЫЙ СПОСОБ (deprecated) ====================
|
||||
|
||||
/*
|
||||
// Было раньше - много разных модулей:
|
||||
import { initUserWebSocket } from './components/userWebSocket.js';
|
||||
import { MessageRenderer } from './components/messageRenderer.js';
|
||||
import { initChatHistoryLoader } from './components/chatHistoryLoader.js';
|
||||
import { initChatMarkRead } from './components/chatMarkRead.js';
|
||||
|
||||
const scrollEl = document.getElementById('chatScroll');
|
||||
const chatId = parseInt(scrollEl.dataset.chatId);
|
||||
|
||||
// Создаем renderer
|
||||
const messageRenderer = new MessageRenderer({
|
||||
containerId: 'chatScroll',
|
||||
currentUserId: userId,
|
||||
profileUrl: profileUrl,
|
||||
detailUrlTemplate: detailUrlTemplate
|
||||
});
|
||||
|
||||
// Инициализируем WebSocket
|
||||
const userWs = initUserWebSocket({ userId });
|
||||
userWs.configure({
|
||||
chatId,
|
||||
messageRenderer,
|
||||
scrollContainerId: 'chatScroll'
|
||||
});
|
||||
|
||||
// Инициализируем History Loader
|
||||
initChatHistoryLoader({
|
||||
messageRenderer,
|
||||
scrollContainerId: 'chatScroll',
|
||||
fetchUrl: `/api/v1/communications/chats/${chatId}/messages/`
|
||||
});
|
||||
|
||||
// И еще кучу других модулей...
|
||||
*/
|
||||
|
||||
// ==================== НОВЫЙ СПОСОБ (recommended) ====================
|
||||
|
||||
/**
|
||||
* Инициализация чата с новой архитектурой
|
||||
*/
|
||||
async function initChat() {
|
||||
// Получаем данные из DOM
|
||||
const scrollEl = document.getElementById('chatScroll');
|
||||
const chatId = parseInt(scrollEl.dataset.chatId);
|
||||
const currentUserId = parseInt(document.body.dataset.userId);
|
||||
|
||||
// Получаем WebSocket (он уже инициализирован в base.html)
|
||||
const ws = window.userWebSocket;
|
||||
|
||||
// Создаем ChatController - ВСЁ в одном!
|
||||
const chatController = new ChatController({
|
||||
chatId,
|
||||
currentUserId,
|
||||
scrollElement: scrollEl,
|
||||
containerId: 'chatScroll',
|
||||
wsConnection: ws,
|
||||
profileUrl: '/employees/profile/',
|
||||
detailUrlTemplate: '/employees/detail/0/'
|
||||
});
|
||||
|
||||
// Инициализируем (загрузка + рендеринг + scroll)
|
||||
await chatController.init();
|
||||
|
||||
// Готово! ChatController управляет всем:
|
||||
// - MessageStore (хранилище)
|
||||
// - MessageLoader (загрузка)
|
||||
// - MessageRenderer (рендеринг)
|
||||
// - ScrollManager (прокрутка)
|
||||
|
||||
// Экспортируем в window для доступа из других модулей
|
||||
window.chatController = chatController;
|
||||
|
||||
console.log('✅ Chat initialized with new architecture!');
|
||||
console.log('Status:', chatController.getStatus());
|
||||
}
|
||||
|
||||
// ==================== ИСПОЛЬЗОВАНИЕ ====================
|
||||
|
||||
/**
|
||||
* Отправка сообщения
|
||||
*/
|
||||
function sendMessage() {
|
||||
const textarea = document.getElementById('messageInput');
|
||||
const content = textarea.value.trim();
|
||||
|
||||
if (!content) return;
|
||||
|
||||
// Просто вызываем метод контроллера
|
||||
const tempId = window.chatController.sendMessage(content);
|
||||
|
||||
console.log('Message sent with temp ID:', tempId);
|
||||
|
||||
// Очищаем textarea
|
||||
textarea.value = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Прокрутка к последнему сообщению
|
||||
*/
|
||||
function scrollToLatest() {
|
||||
window.chatController.scrollToBottom({ instant: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* Загрузка истории
|
||||
*/
|
||||
async function loadMore() {
|
||||
const messages = await window.chatController.loadMoreHistory();
|
||||
console.log('Loaded', messages.length, 'historical messages');
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение статуса чата
|
||||
*/
|
||||
function getStatus() {
|
||||
const status = window.chatController.getStatus();
|
||||
console.log('Chat status:', status);
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Поиск сообщения
|
||||
*/
|
||||
function findMessage(messageId) {
|
||||
const message = window.chatController.getMessage(messageId);
|
||||
console.log('Message:', message);
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Прокрутка к конкретному сообщению
|
||||
*/
|
||||
function goToMessage(messageId) {
|
||||
window.chatController.scrollToMessage(messageId, {
|
||||
block: 'center',
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== ИНТЕГРАЦИЯ С ДРУГИМИ МОДУЛЯМИ ====================
|
||||
|
||||
/**
|
||||
* Интеграция с ChatComposer
|
||||
*/
|
||||
function integrateChatComposer() {
|
||||
// ChatComposer теперь может использовать ChatController
|
||||
const composer = {
|
||||
sendMessage: (content, options) => {
|
||||
return window.chatController.sendMessage(content, options);
|
||||
},
|
||||
|
||||
isNearBottom: () => {
|
||||
return window.chatController.isNearBottom();
|
||||
}
|
||||
};
|
||||
|
||||
return composer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Интеграция с реакциями (reactions)
|
||||
*/
|
||||
window.addEventListener('chat:message-added', (event) => {
|
||||
const { messageId, chatId } = event.detail;
|
||||
|
||||
// Инициализируем реакции для нового сообщения
|
||||
console.log('New message added, initializing reactions:', messageId);
|
||||
|
||||
// MessageContextMenu автоматически подхватит через MutationObserver
|
||||
});
|
||||
|
||||
/**
|
||||
* Интеграция с уведомлениями о прочтении
|
||||
*/
|
||||
window.addEventListener('chat:message-added', (event) => {
|
||||
const { messageId } = event.detail;
|
||||
|
||||
// Проверяем видимо ли сообщение
|
||||
const isVisible = window.chatController.scrollManager.isNearBottom();
|
||||
|
||||
if (isVisible) {
|
||||
// Отмечаем как прочитанное
|
||||
// markAsRead(messageId);
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== ЭКСПОРТ ====================
|
||||
|
||||
export {
|
||||
initChat,
|
||||
sendMessage,
|
||||
scrollToLatest,
|
||||
loadMore,
|
||||
getStatus,
|
||||
findMessage,
|
||||
goToMessage,
|
||||
integrateChatComposer
|
||||
};
|
||||
|
||||
// Автозапуск если это страница чата
|
||||
if (document.getElementById('chatScroll')) {
|
||||
// Ждем когда DOM полностью загрузится
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initChat);
|
||||
} else {
|
||||
initChat();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,85 +0,0 @@
|
||||
{# includes/chat_detail_scripts.html #}
|
||||
{# Скрипты только для страницы chat_detail.html #}
|
||||
{% load static %}
|
||||
|
||||
<script type="module">
|
||||
import { initChatMarkRead } from "{% static 'js/components/chatMarkRead.js' %}";
|
||||
import { initChatComposer } from "{% static 'js/components/chatComposer.js' %}";
|
||||
import { initChatHistoryLoader } from "{% static 'js/components/chatHistoryLoader.js' %}";
|
||||
import { initChatFormManager } from "{% static 'js/components/chatFormManager.js' %}";
|
||||
|
||||
// Ждём готовности chat-системы
|
||||
window.addEventListener('chat:system-ready', (e) => {
|
||||
console.log('[ChatDetailScripts] chat:system-ready event received!', e.detail);
|
||||
const { userWs, avatarMap, badgeManager } = e.detail;
|
||||
|
||||
// ========== FORM MANAGER ==========
|
||||
// Управление режимами формы (send/edit/reply)
|
||||
console.log('[ChatDetailScripts] Initializing FormManager...');
|
||||
const formManager = initChatFormManager({
|
||||
formId: 'chatForm',
|
||||
textareaId: 'id_content',
|
||||
chatId: {{ chat.id }},
|
||||
uploadUrl: "{% url 'api:v1:upload_message' %}",
|
||||
editUrlTemplate: "/api/v1/communications/messages/{id}/edit/"
|
||||
});
|
||||
|
||||
// ========== MARK READ ==========
|
||||
const markReadApi = initChatMarkRead({
|
||||
chatId: {{ chat.id }},
|
||||
meId: {{ user.id|default:"0" }},
|
||||
scrollContainerId: 'chatScroll',
|
||||
textareaId: 'id_content',
|
||||
formId: 'chatForm',
|
||||
scrollBtnId: 'scrollBtn',
|
||||
markReadUrl: "{% url 'communications:chat_mark_read' chat.pk %}",
|
||||
initialLastReadTs: {{ chat.last_read_timestamp|default:"null" }}
|
||||
});
|
||||
|
||||
// ========== COMPOSER ==========
|
||||
console.log('[ChatDetailScripts] Initializing Composer...');
|
||||
const composer = initChatComposer({
|
||||
chatId: {{ chat.id }},
|
||||
formId: 'chatForm',
|
||||
textareaId: 'id_content',
|
||||
uploadUrl: "{% url 'api:v1:upload_message' %}",
|
||||
userWs: userWs
|
||||
});
|
||||
console.log('[ChatDetailScripts] Composer initialized:', composer);
|
||||
|
||||
// ========== HISTORY LOADER ==========
|
||||
const historyLoader = initChatHistoryLoader({
|
||||
chatId: {{ chat.id }},
|
||||
scrollContainerId: 'chatScroll',
|
||||
fetchUrl: "{% url 'api:v1:chat_messages' chat.pk %}",
|
||||
avatarMap: avatarMap.getAll(),
|
||||
meId: {{ user.id }}
|
||||
});
|
||||
|
||||
// Расширяем userWebSocket chat-специфичной конфигурацией
|
||||
if (userWs.configure) {
|
||||
userWs.configure({
|
||||
scrollContainerId: 'chatScroll',
|
||||
formId: 'chatForm',
|
||||
textareaId: 'id_content',
|
||||
typingIndicatorId: 'typing',
|
||||
avatarMap: avatarMap.getAll(),
|
||||
markReadApi: markReadApi
|
||||
});
|
||||
}
|
||||
|
||||
// Открываем чат в WebSocket
|
||||
if (userWs.openChat) {
|
||||
userWs.openChat({{ chat.id }}, false);
|
||||
}
|
||||
|
||||
// Экспортируем API
|
||||
window.chatComposer = composer;
|
||||
window.chatHistoryLoader = historyLoader;
|
||||
window.markReadApi = markReadApi;
|
||||
window.chatFormManager = formManager;
|
||||
});
|
||||
</script>
|
||||
|
||||
{# UI Components #}
|
||||
<script type="module" src="{% static 'js/chat-detail-enhanced.js' %}"></script>
|
||||
@ -1,369 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Test Message Editing - Full Rerender</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
padding: 20px;
|
||||
}
|
||||
.test-section {
|
||||
border: 1px solid #ddd;
|
||||
padding: 15px;
|
||||
margin: 15px 0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.test-section h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#chatScroll {
|
||||
border: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
min-height: 200px;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
.msg {
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.bubble {
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
.bubble-me {
|
||||
background: #e3f2fd;
|
||||
}
|
||||
.message-reactions-wrapper {
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.reply-reference {
|
||||
border-left: 3px solid #2196F3;
|
||||
padding-left: 8px;
|
||||
margin-bottom: 8px;
|
||||
opacity: 0.8;
|
||||
font-size: 13px;
|
||||
}
|
||||
.attachment {
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.message-edited-indicator {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
}
|
||||
.log {
|
||||
background: #000;
|
||||
color: #0f0;
|
||||
padding: 10px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🧪 Test: Message Editing with Full Rerender</h1>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>Текущее состояние</h3>
|
||||
<div id="chatScroll" data-chat-id="1" data-me-id="123"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>Тесты</h3>
|
||||
<button onclick="testSimpleEdit()">1. Простое редактирование текста</button>
|
||||
<button onclick="testWithAttachments()">2. Редактирование с вложениями</button>
|
||||
<button onclick="testWithReply()">3. Редактирование с ответом</button>
|
||||
<button onclick="testRemoveReply()">4. Удаление ответа</button>
|
||||
<button onclick="testWithReactions()">5. Редактирование с реакциями</button>
|
||||
<button onclick="clearChat()">🗑️ Очистить</button>
|
||||
</div>
|
||||
|
||||
<div class="test-section">
|
||||
<h3>Console Log</h3>
|
||||
<div id="log" class="log"></div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { MessageRenderer } from '/static/js/components/messageRenderer.js';
|
||||
|
||||
window.currentUserAvatar = '';
|
||||
window.meId = 123;
|
||||
|
||||
const log = (msg, ...args) => {
|
||||
console.log(msg, ...args);
|
||||
const logEl = document.getElementById('log');
|
||||
logEl.innerHTML += `<div>${new Date().toISOString().substr(11, 8)} - ${msg}</div>`;
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
};
|
||||
|
||||
window.testSimpleEdit = function() {
|
||||
log('TEST 1: Simple text edit');
|
||||
|
||||
const renderer = new MessageRenderer({
|
||||
containerId: 'chatScroll',
|
||||
currentUserId: 123
|
||||
});
|
||||
|
||||
// Исходное сообщение
|
||||
const originalMsg = {
|
||||
id: 1,
|
||||
author_id: 123,
|
||||
author_name: 'Вы',
|
||||
content: 'Привет! Это исходный текст.',
|
||||
created_ts: Date.now(),
|
||||
is_edited: false,
|
||||
reactions_summary: {}
|
||||
};
|
||||
|
||||
renderer.renderMessage(originalMsg);
|
||||
log('✓ Original message rendered');
|
||||
|
||||
setTimeout(() => {
|
||||
// Редактированное сообщение
|
||||
const editedMsg = {
|
||||
...originalMsg,
|
||||
content: 'Привет! Это ОТРЕДАКТИРОВАННЫЙ текст.',
|
||||
is_edited: true,
|
||||
edited_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
log('Simulating WebSocket edit event...');
|
||||
window.dispatchEvent(new CustomEvent('chat:message-edited', {
|
||||
detail: { payload: editedMsg }
|
||||
}));
|
||||
|
||||
setTimeout(() => {
|
||||
const el = document.querySelector('[data-message-id="1"]');
|
||||
const hasIndicator = el?.querySelector('.message-edited-indicator');
|
||||
log(hasIndicator ? '✓ Edit indicator found!' : '✗ Edit indicator NOT found');
|
||||
}, 100);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
window.testWithAttachments = function() {
|
||||
log('TEST 2: Edit with attachments');
|
||||
|
||||
const renderer = new MessageRenderer({
|
||||
containerId: 'chatScroll',
|
||||
currentUserId: 456
|
||||
});
|
||||
|
||||
const msgWithAttachments = {
|
||||
id: 2,
|
||||
author_id: 456,
|
||||
author_name: 'User',
|
||||
content: 'Смотрите фото!',
|
||||
created_ts: Date.now(),
|
||||
is_edited: false,
|
||||
attachments: [
|
||||
{ id: 1, file_name: 'photo1.jpg', file_type: 'image', file_url: '#' },
|
||||
{ id: 2, file_name: 'photo2.jpg', file_type: 'image', file_url: '#' }
|
||||
],
|
||||
reactions_summary: {}
|
||||
};
|
||||
|
||||
renderer.renderMessage(msgWithAttachments);
|
||||
log('✓ Message with 2 attachments rendered');
|
||||
|
||||
setTimeout(() => {
|
||||
const editedMsg = {
|
||||
...msgWithAttachments,
|
||||
content: 'Смотрите ОБНОВЛЕННОЕ фото!',
|
||||
attachments: [
|
||||
{ id: 1, file_name: 'photo1.jpg', file_type: 'image', file_url: '#' }
|
||||
],
|
||||
is_edited: true,
|
||||
edited_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
log('Editing message (removed 1 attachment)...');
|
||||
window.dispatchEvent(new CustomEvent('chat:message-edited', {
|
||||
detail: { payload: editedMsg }
|
||||
}));
|
||||
|
||||
setTimeout(() => {
|
||||
const el = document.querySelector('[data-message-id="2"]');
|
||||
const attachments = el?.querySelectorAll('.attachment, img');
|
||||
log(`Attachments in DOM: ${attachments?.length || 0} (should be 1)`);
|
||||
}, 100);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
window.testWithReply = function() {
|
||||
log('TEST 3: Edit message with reply');
|
||||
|
||||
const renderer = new MessageRenderer({
|
||||
containerId: 'chatScroll',
|
||||
currentUserId: 123
|
||||
});
|
||||
|
||||
const msgWithReply = {
|
||||
id: 3,
|
||||
author_id: 123,
|
||||
author_name: 'Вы',
|
||||
content: 'Согласен!',
|
||||
created_ts: Date.now(),
|
||||
is_edited: false,
|
||||
reply_to: {
|
||||
id: 999,
|
||||
author_name: 'Иван',
|
||||
content: 'Идем завтра на встречу?'
|
||||
},
|
||||
reactions_summary: {}
|
||||
};
|
||||
|
||||
renderer.renderMessage(msgWithReply);
|
||||
log('✓ Message with reply rendered');
|
||||
|
||||
setTimeout(() => {
|
||||
const editedMsg = {
|
||||
...msgWithReply,
|
||||
content: 'Полностью согласен!',
|
||||
is_edited: true,
|
||||
edited_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
log('Editing message (reply preserved)...');
|
||||
window.dispatchEvent(new CustomEvent('chat:message-edited', {
|
||||
detail: { payload: editedMsg }
|
||||
}));
|
||||
|
||||
setTimeout(() => {
|
||||
const el = document.querySelector('[data-message-id="3"]');
|
||||
const replyBlock = el?.querySelector('.reply-reference');
|
||||
log(replyBlock ? '✓ Reply block still present' : '✗ Reply block missing');
|
||||
}, 100);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
window.testRemoveReply = function() {
|
||||
log('TEST 4: Remove reply on edit');
|
||||
|
||||
const renderer = new MessageRenderer({
|
||||
containerId: 'chatScroll',
|
||||
currentUserId: 123
|
||||
});
|
||||
|
||||
const msgWithReply = {
|
||||
id: 4,
|
||||
author_id: 123,
|
||||
author_name: 'Вы',
|
||||
content: 'Отвечаю на вопрос',
|
||||
created_ts: Date.now(),
|
||||
is_edited: false,
|
||||
reply_to: {
|
||||
id: 998,
|
||||
author_name: 'Петр',
|
||||
content: 'Есть вопрос...'
|
||||
},
|
||||
reactions_summary: {}
|
||||
};
|
||||
|
||||
renderer.renderMessage(msgWithReply);
|
||||
log('✓ Message with reply rendered');
|
||||
|
||||
setTimeout(() => {
|
||||
const editedMsg = {
|
||||
...msgWithReply,
|
||||
content: 'Просто сообщение без ответа',
|
||||
reply_to: null, // ← Удалили ответ!
|
||||
is_edited: true,
|
||||
edited_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
log('Editing message (REMOVING reply)...');
|
||||
window.dispatchEvent(new CustomEvent('chat:message-edited', {
|
||||
detail: { payload: editedMsg }
|
||||
}));
|
||||
|
||||
setTimeout(() => {
|
||||
const el = document.querySelector('[data-message-id="4"]');
|
||||
const replyBlock = el?.querySelector('.reply-reference');
|
||||
log(replyBlock ? '✗ Reply block still present (BUG!)' : '✓ Reply block removed');
|
||||
}, 100);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
window.testWithReactions = function() {
|
||||
log('TEST 5: Edit with reactions');
|
||||
|
||||
const renderer = new MessageRenderer({
|
||||
containerId: 'chatScroll',
|
||||
currentUserId: 123
|
||||
});
|
||||
|
||||
const msgWithReactions = {
|
||||
id: 5,
|
||||
author_id: 456,
|
||||
author_name: 'User',
|
||||
content: 'Отличная идея!',
|
||||
created_ts: Date.now(),
|
||||
is_edited: false,
|
||||
reactions_summary: {
|
||||
'👍': { count: 3, users: [1, 2, 3] },
|
||||
'❤️': { count: 1, users: [4] }
|
||||
}
|
||||
};
|
||||
|
||||
renderer.renderMessage(msgWithReactions);
|
||||
log('✓ Message with reactions rendered');
|
||||
|
||||
setTimeout(() => {
|
||||
const editedMsg = {
|
||||
...msgWithReactions,
|
||||
content: 'ОЧЕНЬ отличная идея!',
|
||||
is_edited: true,
|
||||
edited_at: new Date().toISOString(),
|
||||
reactions_summary: {
|
||||
'👍': { count: 5, users: [1, 2, 3, 5, 6] },
|
||||
'❤️': { count: 2, users: [4, 7] },
|
||||
'🔥': { count: 1, users: [8] }
|
||||
}
|
||||
};
|
||||
|
||||
log('Editing message (reactions updated)...');
|
||||
window.dispatchEvent(new CustomEvent('chat:message-edited', {
|
||||
detail: { payload: editedMsg }
|
||||
}));
|
||||
|
||||
setTimeout(() => {
|
||||
const el = document.querySelector('[data-message-id="5"]');
|
||||
const reactionsData = JSON.parse(el?.dataset.reactions || '{}');
|
||||
const reactionCount = Object.keys(reactionsData).length;
|
||||
log(`Reactions in DOM: ${reactionCount} types (should be 3)`);
|
||||
}, 100);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
window.clearChat = function() {
|
||||
document.getElementById('chatScroll').innerHTML = '';
|
||||
document.getElementById('log').innerHTML = '';
|
||||
log('✓ Cleared');
|
||||
};
|
||||
|
||||
// Импортируем и инициализируем messageEditing
|
||||
import { initMessageEditing } from '/static/js/components/messageEditing.js';
|
||||
initMessageEditing();
|
||||
log('✓ Message editing initialized');
|
||||
|
||||
log('Ready to test! Click buttons above.');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,190 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Test MessageRenderer</title>
|
||||
<style>
|
||||
body { font-family: system-ui; padding: 20px; max-width: 900px; margin: 0 auto; }
|
||||
.test { border: 1px solid #ddd; padding: 15px; margin: 15px 0; }
|
||||
pre { background: #f5f5f5; padding: 10px; overflow-x: auto; font-size: 12px; }
|
||||
.result { border: 2px solid #4CAF50; padding: 10px; margin: 10px 0; }
|
||||
button { padding: 8px 16px; margin: 5px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🧪 Test MessageRenderer.buildMessageHtml()</h1>
|
||||
|
||||
<div class="test">
|
||||
<h3>Test 1: Message with Attachments</h3>
|
||||
<button onclick="testAttachments()">Run Test</button>
|
||||
<div id="result1"></div>
|
||||
</div>
|
||||
|
||||
<div class="test">
|
||||
<h3>Test 2: Message with Reply</h3>
|
||||
<button onclick="testReply()">Run Test</button>
|
||||
<div id="result2"></div>
|
||||
</div>
|
||||
|
||||
<div class="test">
|
||||
<h3>Test 3: Message with Forwarded Info</h3>
|
||||
<button onclick="testForwarded()">Run Test</button>
|
||||
<div id="result3"></div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { MessageRenderer } from '/static/js/components/messageRenderer.js';
|
||||
|
||||
window.testAttachments = function() {
|
||||
console.log('TEST 1: Message with attachments');
|
||||
|
||||
const renderer = new MessageRenderer({
|
||||
currentUserId: 123
|
||||
});
|
||||
|
||||
const message = {
|
||||
id: 1,
|
||||
author_id: 123,
|
||||
author_name: 'Test User',
|
||||
content: 'Message with files',
|
||||
created_ts: Date.now(),
|
||||
has_attachments: true,
|
||||
attachments: [
|
||||
{
|
||||
id: 1,
|
||||
file_name: 'document.pdf',
|
||||
file_type: 'file',
|
||||
file_url: '/media/files/document.pdf',
|
||||
file_size: 1024000
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
file_name: 'photo.jpg',
|
||||
file_type: 'image',
|
||||
file_url: '/media/images/photo.jpg'
|
||||
}
|
||||
],
|
||||
reactions_summary: {}
|
||||
};
|
||||
|
||||
const html = renderer.buildMessageHtml(message, true);
|
||||
|
||||
console.log('Generated HTML length:', html.length);
|
||||
console.log('Contains "attachment":', html.includes('attachment'));
|
||||
console.log('Contains "document.pdf":', html.includes('document.pdf'));
|
||||
console.log('Contains "photo.jpg":', html.includes('photo.jpg'));
|
||||
|
||||
document.getElementById('result1').innerHTML = `
|
||||
<div class="result">
|
||||
<strong>Generated HTML:</strong>
|
||||
<pre>${escapeHtml(html)}</pre>
|
||||
<strong>Rendered:</strong>
|
||||
${html}
|
||||
<strong>Checks:</strong>
|
||||
<ul>
|
||||
<li>HTML length: ${html.length} chars</li>
|
||||
<li>Contains "attachment": ${html.includes('attachment') ? '✅' : '❌'}</li>
|
||||
<li>Contains "document.pdf": ${html.includes('document.pdf') ? '✅' : '❌'}</li>
|
||||
<li>Contains "photo.jpg": ${html.includes('photo.jpg') ? '✅' : '❌'}</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
window.testReply = function() {
|
||||
console.log('TEST 2: Message with reply');
|
||||
|
||||
const renderer = new MessageRenderer({
|
||||
currentUserId: 456
|
||||
});
|
||||
|
||||
const message = {
|
||||
id: 2,
|
||||
author_id: 456,
|
||||
author_name: 'User 2',
|
||||
content: 'This is a reply',
|
||||
created_ts: Date.now(),
|
||||
reply_to: {
|
||||
id: 999,
|
||||
author_name: 'Original Author',
|
||||
content: 'Original message text here'
|
||||
},
|
||||
reactions_summary: {}
|
||||
};
|
||||
|
||||
const html = renderer.buildMessageHtml(message, true);
|
||||
|
||||
console.log('Generated HTML length:', html.length);
|
||||
console.log('Contains "reply-reference":', html.includes('reply-reference'));
|
||||
console.log('Contains "Original Author":', html.includes('Original Author'));
|
||||
|
||||
document.getElementById('result2').innerHTML = `
|
||||
<div class="result">
|
||||
<strong>Generated HTML:</strong>
|
||||
<pre>${escapeHtml(html)}</pre>
|
||||
<strong>Rendered:</strong>
|
||||
${html}
|
||||
<strong>Checks:</strong>
|
||||
<ul>
|
||||
<li>Contains "reply-reference": ${html.includes('reply-reference') ? '✅' : '❌'}</li>
|
||||
<li>Contains "Original Author": ${html.includes('Original Author') ? '✅' : '❌'}</li>
|
||||
<li>Contains "Original message text": ${html.includes('Original message text') ? '✅' : '❌'}</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
window.testForwarded = function() {
|
||||
console.log('TEST 3: Message with forwarded info');
|
||||
|
||||
const renderer = new MessageRenderer({
|
||||
currentUserId: 789
|
||||
});
|
||||
|
||||
const message = {
|
||||
id: 3,
|
||||
author_id: 789,
|
||||
author_name: 'User 3',
|
||||
content: 'Forwarded content',
|
||||
created_ts: Date.now(),
|
||||
is_forwarded: true,
|
||||
forwarded_from: {
|
||||
author_name: 'Original Sender',
|
||||
chat_name: 'Some Chat',
|
||||
created_at: '01.12.2025 10:30'
|
||||
},
|
||||
reactions_summary: {}
|
||||
};
|
||||
|
||||
const html = renderer.buildMessageHtml(message, false);
|
||||
|
||||
console.log('Generated HTML length:', html.length);
|
||||
console.log('Contains "forwarded-indicator":', html.includes('forwarded-indicator'));
|
||||
console.log('Contains "Переслано":', html.includes('Переслано'));
|
||||
|
||||
document.getElementById('result3').innerHTML = `
|
||||
<div class="result">
|
||||
<strong>Generated HTML:</strong>
|
||||
<pre>${escapeHtml(html.substring(0, 1000))}...</pre>
|
||||
<strong>Rendered:</strong>
|
||||
${html}
|
||||
<strong>Checks:</strong>
|
||||
<ul>
|
||||
<li>Contains "forwarded-indicator": ${html.includes('forwarded-indicator') ? '✅' : '❌'}</li>
|
||||
<li>Contains "Переслано": ${html.includes('Переслано') ? '✅' : '❌'}</li>
|
||||
<li>Contains "Original Sender": ${html.includes('Original Sender') ? '✅' : '❌'}</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
console.log('✓ Test page ready. Click buttons to run tests.');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user