Corp/MESSAGE_EDITING_ANALYSIS.md

488 lines
17 KiB
Markdown
Raw 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.

# Анализ обработки редактирования сообщений через WebSocket
## Текущая реализация
### Backend (edit_message view)
**Что делает:**
```python
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)
**Что делает:**
```javascript
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 не поддерживает:
**НЕТ обработки вложений:**
```python
# edit_message принимает ТОЛЬКО content
new_content = data.get('content', '').strip()
# ❌ Нет data.get('attachments')
# ❌ Нет data.get('remove_attachments')
# ❌ Нет обработки новых файлов
```
**НЕТ обработки reply_to:**
```python
# ❌ Нет data.get('reply_to_id')
# ❌ Нельзя отменить ответ
# ❌ Нельзя изменить на какое сообщение отвечаем
```
**НЕТ обработки poll:**
```python
# ❌ Нельзя изменить вопрос голосования
# ❌ Нельзя добавить/удалить опции
```
### Вывод: В текущей реализации при редактировании можно менять ТОЛЬКО текст!
## 🔧 РЕКОМЕНДАЦИИ
### Краткосрочные (исправить баги)
#### 1. Обновить `messageEditing.js` для полной перерисовки
**Проблема:** Частичное обновление DOM ненадёжно
**Решение:** Использовать `messageRenderer.js` для полной перерисовки
```javascript
// 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. Альтернатива: Умное частичное обновление
Если полная перерисовка слишком сложна:
```javascript
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` для поддержки вложений
```python
@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 редактирования
```javascript
// В 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 пример:**
```jsx
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 часа):
1. Использовать `messageRenderer.renderMessage()` для полной перерисовки
2. Добавить переинициализацию компонентов
### Полный фикс (1-2 дня):
1. Расширить `edit_message` API для поддержки вложений/reply_to
2. Обновить UI формы редактирования
3. Тестирование всех сценариев
### Идеальное решение (1-2 недели):
1. Мигрировать на React/Vue
2. Централизованное state management
3. Реактивное обновление UI