Corp/MESSAGE_EDITING_ANALYSIS.md

17 KiB
Raw Blame History

Анализ обработки редактирования сообщений через 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 часа):

  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