Corp/REPLY_UPDATE_ON_EDIT.md

326 lines
11 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.

# Обновление Reply-сообщений при редактировании
## Проблема
Когда пользователь редактирует сообщение A, на которое уже ответили сообщением B, то в сообщении B остаётся старый текст из `reply_to` preview.
### Пример:
```
Сообщение #100 (автор: Игорь)
└─ "Оригинальный текст"
Сообщение #101 (автор: Мария, reply_to=100)
├─ [Reply Preview] → "Оригинальный текст" ← это цитата
└─ "Согласна!"
```
Если отредактировать сообщение #100:
```
Сообщение #100
└─ "ИЗМЕНЕННЫЙ текст" ✓ (изменено)
```
То в сообщении #101 должен обновиться reply-preview:
```
Сообщение #101
├─ [Reply Preview] → "ИЗМЕНЕННЫЙ текст" ← должно обновиться!
└─ "Согласна!"
```
## Решение
### 1. Backend: `edit_message` view
**Файл**: `backend/api/v1/communications/views.py`
```python
@csrf_protect
@login_required
@require_POST
def edit_message(request, message_id):
# ... редактируем сообщение ...
# Отправляем 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
}
)
# НОВОЕ: Находим все сообщения, которые ссылаются на это (reply_to)
reply_messages = Message.objects.filter(
reply_to=message, # ← все сообщения, где reply_to указывает на отредактированное
chat=message.chat
).select_related(
'author',
'reply_to', # ← загружаем связанное сообщение (уже с новым content!)
'reply_to__author',
'forwarded_from_author',
'poll'
).prefetch_related(
'attachments',
'reactions',
'reactions__user',
'poll__options'
)
# Отправляем обновления для каждого reply-сообщения
for reply_msg in reply_messages:
reply_payload = serialize_message(reply_msg)
# reply_payload теперь содержит:
# {
# "id": 101,
# "content": "Согласна!",
# "reply_to": {
# "id": 100,
# "content": "ИЗМЕНЕННЫЙ текст", ← новый контент!
# "author_name": "Игорь"
# }
# }
async_to_sync(channel_layer.group_send)(
f'chat_{message.chat_id}',
{
'type': 'chat.message_edited',
'chat_id': message.chat_id,
'payload': reply_payload
}
)
return JsonResponse({'ok': True, 'message': payload})
```
### 2. Frontend: Полная перерисовка через MessageRenderer
**Файл**: `backend/static/js/components/messageEditing.js`
```javascript
function updateMessageInDOM(message, chatId) {
const oldMessageEl = document.querySelector(`[data-message-id="${message.id}"]`);
// MessageRenderer.buildMessageHtml генерирует полный HTML включая:
// - reply_to preview с актуальным content
// - attachments
// - poll
// - reactions
const renderer = new MessageRenderer({...});
const newMessageHtml = renderer.buildMessageHtml(message, isOwn);
// Полная замена элемента
oldMessageEl.replaceWith(newElement);
// Реинициализация компонентов
reinitMessageComponents(newElement, message.id, currentUserId);
}
```
**Файл**: `backend/static/js/components/messageRenderer.js`
```javascript
buildMessageHtml(msg, isOwn) {
// ...
// Reply preview использует msg.reply_to.content напрямую
const replyHtml = msg.reply_to ? `
<div class="reply-reference small mb-2"
style="border-left:3px solid var(--bs-primary);padding-left:8px;">
<div class="fw-semibold">${msg.reply_to.author_name || 'Пользователь'}</div>
<div class="text-truncate">${this.escapeHtml(msg.reply_to.content || '[файл]')}</div>
<!-- ^^^^^^^^^^^^^^^^^^^^ -->
<!-- Берётся из payload! -->
</div>` : '';
// ...
}
```
## Процесс обновления
```
User edits Message #100
↓
POST /api/v1/communications/messages/100/edit/
↓
Backend: edit_message()
├─ message.content = "ИЗМЕНЕННЫЙ текст"
├─ message.save()
├─ message = reload with prefetch (reply_to included)
│
├─ Send WebSocket: message_edited (Message #100)
│ payload.content = "ИЗМЕНЕННЫЙ текст"
│
└─ Find reply_messages = Message.objects.filter(reply_to=100)
├─ Found: Message #101
├─ reload #101 with prefetch (includes reply_to with NEW content!)
└─ Send WebSocket: message_edited (Message #101)
payload = {
id: 101,
content: "Согласна!",
reply_to: {
id: 100,
content: "ИЗМЕНЕННЫЙ текст" ← UPDATED!
}
}
↓
WebSocket → user_consumer.py
├─ Receives: message_edited (#100)
└─ Receives: message_edited (#101)
↓
WebSocket → userWebSocket.js
├─ handleMessageUpdated() for #100
│ └─ dispatchEvent('chat:message-edited', payload=#100)
└─ handleMessageUpdated() for #101
└─ dispatchEvent('chat:message-edited', payload=#101)
↓
messageEditing.js
├─ handleMessageEdited(#100)
│ └─ updateMessageInDOM(#100) → full rerender
│ └─ "ИЗМЕНЕННЫЙ текст" + "(изменено)"
│
└─ handleMessageEdited(#101)
└─ updateMessageInDOM(#101) → full rerender
└─ Reply preview shows "ИЗМЕНЕННЫЙ текст" ✓
```
## Тестирование
### Шаг 1: Подготовка данных
```bash
cd backend
python test_reply_update.py
```
Это создаст:
- Сообщение A (оригинал)
- Сообщение B (ответ на A)
### Шаг 2: Открыть чат
1. Откройте чат с этими сообщениями
2. Откройте консоль браузера (F12)
### Шаг 3: Отредактировать сообщение A
1. Нажмите редактирование на сообщении A
2. Измените текст
3. Сохраните
### Шаг 4: Проверить логи
**Backend (terminal)**:
```
[EDIT_MSG] Message 100 edited, found 1 reply messages
[EDIT_MSG] Updating reply message 101 (reply_to.content='ИЗМЕНЕННЫЙ текст')
```
**Frontend (browser console)**:
```
[UserWebSocket] Received message: message_updated (id=100)
[UserWebSocket] Dispatching chat:message-edited event...
[MessageEditing] Message edited event received
[MessageEditing] message.id: 100
[UserWebSocket] Received message: message_updated (id=101)
[UserWebSocket] Dispatching chat:message-edited event...
[MessageEditing] Message edited event received
[MessageEditing] message.id: 101
[MessageEditing] message.reply_to: {id: 100, content: 'ИЗМЕНЕННЫЙ текст', ...}
```
### Шаг 5: Проверить UI
✅ Сообщение A: новый текст + "(изменено)"
✅ Сообщение B: reply-preview показывает **новый** текст из A
## Граничные случаи
### Множественные ответы
Если на сообщение A ответили 5 раз (сообщения B, C, D, E, F), то все 5 получат обновление:
```python
reply_messages = Message.objects.filter(reply_to=message) # → 5 messages
for reply_msg in reply_messages: # → 5 WebSocket events
send message_edited
```
### Вложенные ответы (reply на reply)
```
Message #100 (A)
└─ Message #101 (B, reply_to=100)
└─ Message #102 (C, reply_to=101)
```
При редактировании #100:
- ✅ #101 обновится (reply_to=100)
- ❌ #102 НЕ обновится (reply_to=101, а не 100)
Это **правильное** поведение, так как #102 ссылается на #101, а не на #100.
### Редактирование в другом чате
```python
reply_messages = Message.objects.filter(
reply_to=message,
chat=message.chat # ← только в этом же чате
)
```
Если теоретически reply_to может указывать на сообщение из другого чата (пересланное), то оно **не обновится**. Это безопасное поведение.
## Производительность
### Оптимизация: batch send
Текущая реализация:
```python
for reply_msg in reply_messages: # N запросов
send message_edited
```
Можно оптимизировать:
```python
reply_payloads = [serialize_message(m) for m in reply_messages]
async_to_sync(channel_layer.group_send)(
f'chat_{message.chat_id}',
{
'type': 'chat.messages_batch_edited',
'payloads': reply_payloads # Одно событие с массивом
}
)
```
Но для типичных случаев (1-3 ответа) текущая реализация достаточно эффективна.
## Изменённые файлы
1. ✅ `backend/api/v1/communications/views.py`
- Добавлено: поиск reply_messages
- Добавлено: отправка message_edited для каждого reply
- Добавлено: логирование
2. ✅ `backend/static/js/components/userWebSocket.js`
- Исправлено: dispatch events для messageEditing.js
3. ✅ `backend/static/js/components/messageEditing.js`
- Используется: полная перерисовка через MessageRenderer
4. ✅ `backend/static/js/components/messageRenderer.js`
- Использует: `msg.reply_to.content` для preview
## Итог
✅ При редактировании сообщения все ответы на него **автоматически обновляются**
✅ Reply-preview показывает актуальный контент
✅ Работает через существующую систему WebSocket events
✅ Использует полную перерисовку (гарантирует корректность)