updateMessage method

void updateMessage(
  1. ChatMessage message
)

Updates an existing message or adds it if not found.

Useful for updating streaming messages or editing existing ones.

Implementation

void updateMessage(final ChatMessage message) {
  try {
    // Get the message ID - first from customProperties, then calculate if not present
    final customId = message.customProperties?['id'] as String?;
    final messageId = customId ?? _getMessageId(message);

    // Check if the message exists
    final index = _messages.indexWhere(
      (final msg) => _getMessageId(msg) == messageId,
    );

    final isStreaming = message.customProperties?['isStreaming'] as bool? ?? false;

    // Check if this is a user message
    final isUserMessage = message.customProperties?['isUserMessage'] as bool? ??
        message.customProperties?['source'] == 'user';

    // When updating streaming messages, make sure we maintain proper state transitions
    if (index != -1 && isStreaming) {
      // For streaming messages, preserve the original streaming state if present
      final existingIsStreaming =
          _messages[index].customProperties?['isStreaming'] as bool? ?? false;

      // Fix: Preserve the isFirstResponseMessage and isStartOfResponse flags during updates
      final existingIsFirstResponse =
          _messages[index].customProperties?['isFirstResponseMessage'] as bool? ?? false;
      final existingIsStartOfResponse =
          _messages[index].customProperties?['isStartOfResponse'] as bool? ?? false;

      // Create updated properties with preserved flags
      final updatedProperties = {...?message.customProperties};

      // Preserve the response flags during streaming updates
      if (existingIsFirstResponse) {
        updatedProperties['isFirstResponseMessage'] = true;
      }

      if (existingIsStartOfResponse) {
        updatedProperties['isStartOfResponse'] = true;
      }

      // Only override the streaming state if explicitly set to false (indicating end of stream)
      if (existingIsStreaming && isStreaming) {
        // Keep streaming active - preserve existing ID and streaming flag
        _messages[index] = message.copyWith(customProperties: updatedProperties);
        _messageCache[messageId] = _messages[index];
      } else {
        // End of streaming or non-streaming update - regular update
        _messages[index] = message.copyWith(customProperties: updatedProperties);
        _messageCache[messageId] = _messages[index];
      }
    } else if (index != -1) {
      // Regular non-streaming message update
      // Fix: Also preserve response flags for non-streaming updates
      final existingIsFirstResponse =
          _messages[index].customProperties?['isFirstResponseMessage'] as bool? ?? false;
      final existingIsStartOfResponse =
          _messages[index].customProperties?['isStartOfResponse'] as bool? ?? false;

      // Create updated properties with preserved flags
      final updatedProperties = {...?message.customProperties};

      if (existingIsFirstResponse) {
        updatedProperties['isFirstResponseMessage'] = true;
      }

      if (existingIsStartOfResponse) {
        updatedProperties['isStartOfResponse'] = true;
      }

      _messages[index] = message.copyWith(customProperties: updatedProperties);
      _messageCache[messageId] = _messages[index];
    } else {
      // Add new message if not found - respecting list order
      // For new messages being created directly through updateMessage (rare case),
      // preserve any isStartOfResponse flag that might be set
      final newMsgProperties = {...?message.customProperties};

      // If this is explicitly marked as start of response, make it consistent
      if (newMsgProperties['isStartOfResponse'] == true) {
        newMsgProperties['isFirstResponseMessage'] = true;
        _currentResponseFirstMessageId = messageId;
      }

      final updatedMessage = message.copyWith(customProperties: newMsgProperties);

      if (paginationConfig.reverseOrder) {
        _messages.insert(0, updatedMessage);
      } else {
        _messages.add(updatedMessage);
      }
      _messageCache[messageId] = updatedMessage;
    }

    // Safe notification and scrolling strategy
    if (isStreaming) {
      _isCurrentlyStreaming = true;
      // For streaming: just notify, no scrolling to prevent assertion errors
      notifyListeners();
    } else {
      // If we were streaming and now we're not, this is the end of stream
      final wasStreaming = _isCurrentlyStreaming;
      _isCurrentlyStreaming = false;

      // Always notify listeners
      notifyListeners();

      // Only scroll if configured to do so and not during rapid updates
      final config = scrollBehaviorConfig;
      var shouldScroll = false;
      switch (config.autoScrollBehavior) {
        case AutoScrollBehavior.always:
          // For streaming end, scroll after a delay to prevent assertion errors
          shouldScroll = true;
          break;
        case AutoScrollBehavior.onNewMessage:
          // Only scroll on truly new messages (index == -1) or when streaming ends
          shouldScroll = index == -1 || wasStreaming;
          break;
        case AutoScrollBehavior.onUserMessageOnly:
          shouldScroll = isUserMessage;
          break;
        case AutoScrollBehavior.never:
          shouldScroll = false;
          break;
      }

      if (shouldScroll && wasStreaming) {
        // For streaming end, use a longer delay to prevent assertion errors
        // Cancel any pending scroll timer first
        _pendingScrollTimer?.cancel();
        _pendingScrollTimer = Timer(const Duration(milliseconds: 500), () {
          if (mounted && _scrollController?.hasClients == true) {
            _scrollAfterRender(isUserMessage, false, config);
          }
          _pendingScrollTimer = null;
        });
      } else if (shouldScroll) {
        _scrollAfterRender(isUserMessage, false, config);
      }
    }
  } catch (e) {
    debugPrint('Error updating message: $e');
    // If updating fails, try to add as a new message instead
    try {
      final newId = '${message.user.id}_${DateTime.now().millisecondsSinceEpoch}';
      final messageWithId = ChatMessage(
        text: message.text,
        user: message.user,
        createdAt: message.createdAt,
        isMarkdown: message.isMarkdown,
        customProperties: {...?message.customProperties, 'id': newId},
      );

      if (paginationConfig.reverseOrder) {
        _messages.insert(0, messageWithId);
      } else {
        _messages.add(messageWithId);
      }
      _messageCache[newId] = messageWithId;
      notifyListeners();

      // Only scroll if configured to do so for new messages
      final config = scrollBehaviorConfig;

      // Detect if this is a user message
      final isUserMessage = message.customProperties?['isUserMessage'] as bool? ??
          message.customProperties?['source'] == 'user';

      // Identify the first message in a response chain
      final isFirstResponse =
          message.customProperties?['isFirstResponseMessage'] as bool? ?? false;

      var shouldScroll = false;
      switch (config.autoScrollBehavior) {
        case AutoScrollBehavior.always:
        case AutoScrollBehavior.onNewMessage:
          shouldScroll = !isUserMessage && !isFirstResponse;
          break;
        case AutoScrollBehavior.onUserMessageOnly:
          shouldScroll = isUserMessage && !isFirstResponse;
          break;
        case AutoScrollBehavior.never:
          shouldScroll = false;
          break;
      }

      if (shouldScroll) {
        _scrollAfterRender(false, false, config);
      } else if (isUserMessage && isFirstResponse) {
        debugPrint('SKIPPING SCROLL: Custom scroll behavior is active');
      }
    } catch (fallbackError) {
      debugPrint('Failed to add message as fallback: $fallbackError');
    }
  }
}