11 KiB
Обновление 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
@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
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
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: Подготовка данных
cd backend
python test_reply_update.py
Это создаст:
- Сообщение A (оригинал)
- Сообщение B (ответ на A)
Шаг 2: Открыть чат
- Откройте чат с этими сообщениями
- Откройте консоль браузера (F12)
Шаг 3: Отредактировать сообщение A
- Нажмите редактирование на сообщении A
- Измените текст
- Сохраните
Шаг 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 получат обновление:
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.
Редактирование в другом чате
reply_messages = Message.objects.filter(
reply_to=message,
chat=message.chat # ← только в этом же чате
)
Если теоретически reply_to может указывать на сообщение из другого чата (пересланное), то оно не обновится. Это безопасное поведение.
Производительность
Оптимизация: batch send
Текущая реализация:
for reply_msg in reply_messages: # N запросов
send message_edited
Можно оптимизировать:
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 ответа) текущая реализация достаточно эффективна.
Изменённые файлы
-
✅
backend/api/v1/communications/views.py- Добавлено: поиск reply_messages
- Добавлено: отправка message_edited для каждого reply
- Добавлено: логирование
-
✅
backend/static/js/components/userWebSocket.js- Исправлено: dispatch events для messageEditing.js
-
✅
backend/static/js/components/messageEditing.js- Используется: полная перерисовка через MessageRenderer
-
✅
backend/static/js/components/messageRenderer.js- Использует:
msg.reply_to.contentдля preview
- Использует:
Итог
✅ При редактировании сообщения все ответы на него автоматически обновляются ✅ Reply-preview показывает актуальный контент ✅ Работает через существующую систему WebSocket events ✅ Использует полную перерисовку (гарантирует корректность)