updateMessage method
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');
}
}
}