488 lines
17 KiB
Markdown
488 lines
17 KiB
Markdown
# Анализ обработки редактирования сообщений через 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
|