17 KiB
17 KiB
Анализ обработки редактирования сообщений через WebSocket
Текущая реализация
Backend (edit_message view)
Что делает:
def edit_message(request, message_id):
# 1. Получает новый текст из request
new_content = data.get('content', '').strip()
# 2. Находит сообщение
message = Message.objects.get(pk=message_id, author=request.user)
# 3. Сохраняет историю
message.edit_history.append({
'timestamp': timezone.now().isoformat(),
'old_content': message.content
})
# 4. Обновляет ТОЛЬКО текст
message.content = new_content
message.is_edited = True
message.edited_at = timezone.now()
message.save()
# 5. Отправляет через WebSocket
payload = serialize_message(message) # ← ПОЛНАЯ сериализация!
Что включает serialize_message:
- ✅ Базовая информация (id, content, author)
- ✅ Вложения (
attachments) - полный список - ✅ Ответ на сообщение (
reply_to) - вся информация - ✅ Реакции (
reactions_summary) - ✅ Голосования (
poll) - ✅ Информация о пересылке (
forwarded_from) - ✅ Флаги (is_edited, is_deleted, is_pinned, etc.)
Frontend (messageEditing.js)
Что делает:
function updateMessageInDOM(message) {
// ❌ ПРОБЛЕМА: Обновляет ТОЛЬКО текст!
const bubbleEl = messageEl.querySelector('.chat-bubble__text, .message-bubble__text');
if (bubbleEl) {
bubbleEl.textContent = message.content; // ← ТОЛЬКО ТЕКСТ!
}
// ✅ Добавляет индикатор "(изменено)"
if (message.is_edited) {
// ... создает editedIndicator
}
}
🔴 ПРОБЛЕМЫ
1. Вложения НЕ обновляются
Что происходит:
- Backend отправляет полный
message.attachments[]через WebSocket - Frontend игнорирует это поле
- Если при редактировании удалили/добавили файлы - DOM не обновится
Пример сценария:
1. Сообщение: "Смотри фото" + [image1.jpg, image2.jpg]
2. Редактирование: "Смотри фото" (удалили image2.jpg)
3. WebSocket отправляет: { content: "...", attachments: [{image1.jpg}] }
4. Frontend обновляет: ❌ Текст изменен, но image2.jpg всё ещё отображается!
2. Reply-to НЕ обновляется
Что происходит:
- Backend отправляет
message.reply_to(может быть null если ответ отменили) - Frontend игнорирует это поле
- Если при редактировании отменили ответ - блок reply всё ещё показывается
Пример сценария:
1. Сообщение: [reply-to: msg#123] "Согласен"
2. Редактирование: убрали ответ, текст "Согласен"
3. WebSocket: { content: "Согласен", reply_to: null }
4. Frontend: ❌ Блок reply_to всё ещё показывается!
3. Голосования НЕ обновляются
Что происходит:
- Backend отправляет полные данные
message.poll - Frontend игнорирует poll
- Если отредактировали вопрос или опции - не обновится
4. Реакции МОГУТ потеряться
Что происходит:
- Backend отправляет
reactions_summary - Frontend НЕ обновляет реакции при редактировании
- Если между редактированием кто-то добавил реакцию - может потеряться
⚙️ Что МОЖНО при редактировании
Backend не поддерживает:
НЕТ обработки вложений:
# edit_message принимает ТОЛЬКО content
new_content = data.get('content', '').strip()
# ❌ Нет data.get('attachments')
# ❌ Нет data.get('remove_attachments')
# ❌ Нет обработки новых файлов
НЕТ обработки reply_to:
# ❌ Нет data.get('reply_to_id')
# ❌ Нельзя отменить ответ
# ❌ Нельзя изменить на какое сообщение отвечаем
НЕТ обработки poll:
# ❌ Нельзя изменить вопрос голосования
# ❌ Нельзя добавить/удалить опции
Вывод: В текущей реализации при редактировании можно менять ТОЛЬКО текст!
🔧 РЕКОМЕНДАЦИИ
Краткосрочные (исправить баги)
1. Обновить messageEditing.js для полной перерисовки
Проблема: Частичное обновление DOM ненадёжно
Решение: Использовать messageRenderer.js для полной перерисовки
// messageEditing.js
import messageRenderer from './messageRenderer.js';
function updateMessageInDOM(message) {
const messageElements = document.querySelectorAll(
`[data-message-id="${message.id}"]`
);
if (messageElements.length === 0) return;
messageElements.forEach(oldMessageEl => {
// ПОЛНАЯ перерисовка сообщения
const newMessageHTML = messageRenderer.renderMessage(message, {
currentUserId: window.meId,
chatId: window.chatId
});
// Заменяем HTML целиком
const tempDiv = document.createElement('div');
tempDiv.innerHTML = newMessageHTML;
const newMessageEl = tempDiv.firstElementChild;
// Сохраняем скролл позицию
const scrollContainer = document.getElementById('chatScroll');
const scrollBefore = scrollContainer.scrollTop;
// Заменяем элемент
oldMessageEl.replaceWith(newMessageEl);
// Восстанавливаем скролл
scrollContainer.scrollTop = scrollBefore;
// Переинициализируем компоненты
reinitMessageComponents(newMessageEl, message.id);
});
}
function reinitMessageComponents(messageEl, messageId) {
// Реакции
if (window.reactions) {
window.reactions.initMessageReactions(
messageEl,
messageId,
window.meId
);
}
// Контекстное меню (уже работает через делегирование)
// Attachments (если есть lightbox и т.д.)
}
Преимущества:
- ✅ Вложения обновятся автоматически
- ✅ Reply-to обновится автоматически
- ✅ Poll обновится автоматически
- ✅ Все стили и классы применятся правильно
- ✅ Не нужно дублировать логику рендеринга
Недостатки:
- ⚠️ Требует проверки что
messageRenderer.renderMessage()экспортирован - ⚠️ Нужно убедиться что все компоненты переинициализируются
2. Альтернатива: Умное частичное обновление
Если полная перерисовка слишком сложна:
function updateMessageInDOM(message) {
const messageEl = document.querySelector(`[data-message-id="${message.id}"]`);
if (!messageEl) return;
// 1. Обновляем текст
updateTextContent(messageEl, message);
// 2. Обновляем вложения
if (message.has_attachments) {
updateAttachments(messageEl, message.attachments);
} else {
removeAllAttachments(messageEl);
}
// 3. Обновляем reply-to
if (message.reply_to) {
updateReplyBlock(messageEl, message.reply_to);
} else {
removeReplyBlock(messageEl);
}
// 4. Обновляем poll
if (message.poll) {
updatePollBlock(messageEl, message.poll);
}
// 5. Обновляем индикатор редактирования
updateEditedIndicator(messageEl, message.is_edited);
}
function updateAttachments(messageEl, attachments) {
let attachmentsContainer = messageEl.querySelector('.message-attachments');
if (!attachmentsContainer) {
// Создаём контейнер если его нет
attachmentsContainer = document.createElement('div');
attachmentsContainer.className = 'message-attachments mt-2';
messageEl.querySelector('.message-bubble').appendChild(attachmentsContainer);
}
// Очищаем и рендерим заново
attachmentsContainer.innerHTML = '';
attachments.forEach(att => {
const attEl = renderAttachment(att);
attachmentsContainer.appendChild(attEl);
});
}
function removeAllAttachments(messageEl) {
const container = messageEl.querySelector('.message-attachments');
if (container) {
container.remove();
}
}
function updateReplyBlock(messageEl, replyData) {
let replyBlock = messageEl.querySelector('.message-reply-to');
if (!replyBlock) {
// Создаём блок если его нет
replyBlock = document.createElement('div');
replyBlock.className = 'message-reply-to';
const bubble = messageEl.querySelector('.message-bubble');
bubble.insertBefore(replyBlock, bubble.firstChild);
}
replyBlock.innerHTML = `
<div class="reply-preview">
<strong>${replyData.author_name}</strong>
<p class="text-muted">${replyData.content}</p>
</div>
`;
}
function removeReplyBlock(messageEl) {
const replyBlock = messageEl.querySelector('.message-reply-to');
if (replyBlock) {
replyBlock.remove();
}
}
Среднесрочные (функциональность)
3. Расширить API edit_message для поддержки вложений
@csrf_protect
@login_required
@require_POST
def edit_message(request, message_id):
"""Редактирование сообщения с поддержкой вложений и reply_to"""
import json
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({'ok': False, 'error': 'Invalid JSON'}, status=400)
message = get_object_or_404(Message, pk=message_id, author=request.user)
# Обновляем контент
new_content = data.get('content', '').strip()
if new_content:
message.content = new_content
# НОВОЕ: Обработка reply_to
if 'reply_to_id' in data:
reply_to_id = data.get('reply_to_id')
if reply_to_id is None:
# Отменяем ответ
message.reply_to = None
elif reply_to_id:
# Проверяем существование сообщения
try:
reply_msg = Message.objects.get(
pk=reply_to_id,
chat=message.chat
)
message.reply_to = reply_msg
except Message.DoesNotExist:
return JsonResponse({
'ok': False,
'error': 'Reply message not found'
}, status=400)
# НОВОЕ: Обработка вложений
if 'remove_attachments' in data:
# Удаляем указанные вложения
remove_ids = data.get('remove_attachments', [])
MessageAttachment.objects.filter(
message=message,
id__in=remove_ids
).delete()
# Обновляем флаг
message.has_attachments = message.attachments.exists()
# Сохраняем историю
message.edit_history.append({
'timestamp': timezone.now().isoformat(),
'old_content': message.content,
'changes': {
'content': 'content' in data,
'reply_to': 'reply_to_id' in data,
'attachments': 'remove_attachments' in data
}
})
message.is_edited = True
message.edited_at = timezone.now()
message.save()
# Отправляем обновление через WebSocket
channel_layer = get_channel_layer()
payload = serialize_message(message)
async_to_sync(channel_layer.group_send)(
f'chat_{message.chat_id}',
{
'type': 'chat.message_edited',
'chat_id': message.chat_id,
'payload': payload
}
)
return JsonResponse({'ok': True, 'message': payload})
4. Обновить UI редактирования
// В messageContextMenu.js или где у вас форма редактирования
async function submitEditedMessage(messageId, newContent, options = {}) {
const payload = {
content: newContent
};
// НОВОЕ: Поддержка отмены ответа
if (options.removeReply) {
payload.reply_to_id = null;
}
// НОВОЕ: Поддержка удаления вложений
if (options.removeAttachments && options.removeAttachments.length) {
payload.remove_attachments = options.removeAttachments;
}
const response = await fetch(
`/api/v1/communications/messages/${messageId}/edit/`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify(payload)
}
);
return response.json();
}
Долгосрочные (архитектура)
5. Использовать React/Vue для реактивного рендеринга
Текущий подход (императивный DOM) сложен в поддержке:
- Нужно вручную синхронизировать все изменения
- Легко упустить какое-то поле
- Дублирование логики рендеринга
React пример:
function Message({ message, currentUserId }) {
return (
<div data-message-id={message.id}>
{message.reply_to && (
<ReplyBlock reply={message.reply_to} />
)}
<MessageBubble
content={message.content}
author={message.author_name}
isEdited={message.is_edited}
/>
{message.attachments && (
<Attachments files={message.attachments} />
)}
{message.poll && (
<Poll data={message.poll} />
)}
<Reactions
summary={message.reactions_summary}
currentUserId={currentUserId}
/>
</div>
);
}
// При получении WebSocket события:
function handleMessageEdited(event) {
const updatedMessage = event.detail.payload;
// React автоматически перерисует только изменённые части!
setState(prevState => ({
...prevState,
messages: prevState.messages.map(m =>
m.id === updatedMessage.id ? updatedMessage : m
)
}));
}
📋 ИТОГО
Текущее состояние:
- ❌ Backend отправляет полные данные, но Frontend обновляет только текст
- ❌ Вложения не обновляются
- ❌ Reply-to не обновляется
- ❌ Poll не обновляется
- ❌ Backend не позволяет редактировать что-либо кроме текста
Минимальный фикс (1-2 часа):
- Использовать
messageRenderer.renderMessage()для полной перерисовки - Добавить переинициализацию компонентов
Полный фикс (1-2 дня):
- Расширить
edit_messageAPI для поддержки вложений/reply_to - Обновить UI формы редактирования
- Тестирование всех сценариев
Идеальное решение (1-2 недели):
- Мигрировать на React/Vue
- Централизованное state management
- Реактивное обновление UI