import 'dart:convert'; import 'package:uuid/uuid.dart'; import '../utils/openwebui_source_parser.dart'; /// Utilities for converting OpenWebUI conversation payloads into JSON maps /// that match the app's `Conversation` / `ChatMessage` schemas. All helpers /// here are isolate-safe (they only work with primitive JSON types) so they /// can be executed inside a background worker. const _uuid = Uuid(); Map parseConversationSummary(Map chatData) { final id = (chatData['id'] ?? '').toString(); final title = _stringOr(chatData['title'], 'Chat'); final updatedAtRaw = chatData['updated_at'] ?? chatData['updatedAt']; final createdAtRaw = chatData['created_at'] ?? chatData['createdAt']; final pinned = _safeBool(chatData['pinned']) ?? false; final archived = _safeBool(chatData['archived']) ?? false; final shareId = chatData['share_id']?.toString(); final folderId = chatData['folder_id']?.toString(); String? systemPrompt; final chatObject = chatData['chat']; if (chatObject is Map) { final value = chatObject['system']; if (value is String && value.trim().isNotEmpty) { systemPrompt = value; } } else if (chatData['system'] is String) { final value = (chatData['system'] as String).trim(); if (value.isNotEmpty) systemPrompt = value; } return { 'id': id, 'title': title, 'createdAt': _parseTimestamp(createdAtRaw).toIso8601String(), 'updatedAt': _parseTimestamp(updatedAtRaw).toIso8601String(), 'model': chatData['model']?.toString(), 'systemPrompt': systemPrompt, 'messages': const >[], 'metadata': _coerceJsonMap(chatData['metadata']), 'pinned': pinned, 'archived': archived, 'shareId': shareId, 'folderId': folderId, 'tags': _coerceStringList(chatData['tags']), }; } Map parseFullConversation(Map chatData) { final id = (chatData['id'] ?? '').toString(); final title = _stringOr(chatData['title'], 'Chat'); final updatedAt = _parseTimestamp( chatData['updated_at'] ?? chatData['updatedAt'], ); final createdAt = _parseTimestamp( chatData['created_at'] ?? chatData['createdAt'], ); final pinned = _safeBool(chatData['pinned']) ?? false; final archived = _safeBool(chatData['archived']) ?? false; final shareId = chatData['share_id']?.toString(); final folderId = chatData['folder_id']?.toString(); String? systemPrompt; final chatObject = chatData['chat']; if (chatObject is Map) { final value = chatObject['system']; if (value is String && value.trim().isNotEmpty) { systemPrompt = value; } } else if (chatData['system'] is String) { final value = (chatData['system'] as String).trim(); if (value.isNotEmpty) systemPrompt = value; } String? model; Map? historyMessagesMap; List>? messagesList; if (chatObject is Map) { final history = chatObject['history']; if (history is Map) { if (history['messages'] is Map) { historyMessagesMap = history['messages'] as Map; messagesList = _buildMessagesListFromHistory(history); } } if ((messagesList == null || messagesList.isEmpty) && chatObject['messages'] is List) { messagesList = (chatObject['messages'] as List) .whereType>() .toList(); } final models = chatObject['models']; if (models is List && models.isNotEmpty) { model = models.first?.toString(); } } if ((messagesList == null || messagesList.isEmpty) && chatData['messages'] is List) { messagesList = (chatData['messages'] as List) .whereType>() .toList(); } final messages = >[]; if (messagesList != null) { var index = 0; while (index < messagesList.length) { final msgData = Map.from(messagesList[index]); final historyMsg = historyMessagesMap != null ? (historyMessagesMap[msgData['id']] as Map?) : null; final toolCalls = _extractToolCalls(msgData, historyMsg); if ((msgData['role']?.toString() ?? '') == 'assistant' && toolCalls != null) { final results = >[]; var j = index + 1; while (j < messagesList.length) { final nextRaw = messagesList[j]; if ((nextRaw['role']?.toString() ?? '') != 'tool') break; results.add({ 'tool_call_id': nextRaw['tool_call_id']?.toString(), 'content': nextRaw['content'], if (nextRaw.containsKey('files')) 'files': nextRaw['files'], }); j++; } final synthesized = _synthesizeToolDetailsFromToolCallsWithResults( toolCalls, results, ); final merged = Map.from(msgData); if (synthesized.isNotEmpty) { merged['content'] = synthesized; } final parsed = _parseOpenWebUIMessageToJson( merged, historyMsg: historyMsg, ); // Add versions from siblings _addVersionsFromSiblings(parsed, msgData, historyMessagesMap); messages.add(parsed); index = j; continue; } final parsed = _parseOpenWebUIMessageToJson( msgData, historyMsg: historyMsg, ); // Add versions from siblings _addVersionsFromSiblings(parsed, msgData, historyMessagesMap); messages.add(parsed); index++; } } return { 'id': id, 'title': title, 'createdAt': createdAt.toIso8601String(), 'updatedAt': updatedAt.toIso8601String(), 'model': model, 'systemPrompt': systemPrompt, 'messages': messages, 'metadata': _coerceJsonMap(chatData['metadata']), 'pinned': pinned, 'archived': archived, 'shareId': shareId, 'folderId': folderId, 'tags': _coerceStringList(chatData['tags']), }; } List>? _extractToolCalls( Map msgData, Map? historyMsg, ) { final toolCallsRaw = msgData['tool_calls'] ?? historyMsg?['tool_calls'] ?? historyMsg?['toolCalls']; if (toolCallsRaw is List) { return toolCallsRaw.whereType().map(_coerceJsonMap).toList(); } return null; } /// Add versions from sibling messages (alternative responses with same parent). /// Siblings are stored in `_siblings` by `_buildMessagesListFromHistory`. void _addVersionsFromSiblings( Map parsed, Map msgData, Map? historyMessagesMap, ) { final siblings = msgData['_siblings']; if (siblings is! List || siblings.isEmpty) return; final versions = >[]; for (final siblingData in siblings) { if (siblingData is! Map) continue; final siblingId = siblingData['id']?.toString(); final historyMsg = historyMessagesMap != null && siblingId != null ? (historyMessagesMap[siblingId] as Map?) : null; // Parse the sibling as a version final version = _parseSiblingAsVersion(siblingData, historyMsg: historyMsg); if (version != null) { versions.add(version); } } if (versions.isNotEmpty) { parsed['versions'] = versions; } } /// Parse a sibling message as a ChatMessageVersion JSON map. Map? _parseSiblingAsVersion( Map msgData, { Map? historyMsg, }) { // Extract content (same logic as _parseOpenWebUIMessageToJson) dynamic content = msgData['content']; if ((content == null || (content is String && content.isEmpty)) && historyMsg != null && historyMsg['content'] != null) { content = historyMsg['content']; } var contentString = ''; if (content is List) { final buffer = StringBuffer(); for (final entry in content) { if (entry is Map && entry['type'] == 'text') { final text = entry['text']?.toString(); if (text != null && text.isNotEmpty) { buffer.write(text); } } } contentString = buffer.toString(); } else { contentString = content?.toString() ?? ''; } if (historyMsg != null) { final histContent = historyMsg['content']; if (histContent is String && histContent.length > contentString.length) { contentString = histContent; } } // Extract files final effectiveFiles = msgData['files'] ?? historyMsg?['files']; List>? files; if (effectiveFiles is List) { final allFiles = >[]; for (final entry in effectiveFiles) { if (entry is! Map) continue; if (entry['type'] != null && entry['url'] != null) { final fileMap = { 'type': entry['type'], 'url': entry['url'], }; if (entry['name'] != null) fileMap['name'] = entry['name']; if (entry['size'] != null) fileMap['size'] = entry['size']; allFiles.add(fileMap); } } files = allFiles.isNotEmpty ? allFiles : null; } // Extract other fields final sourcesRaw = historyMsg != null ? historyMsg['sources'] ?? historyMsg['citations'] : msgData['sources'] ?? msgData['citations']; final followUpsRaw = historyMsg != null ? historyMsg['followUps'] ?? historyMsg['follow_ups'] : msgData['followUps'] ?? msgData['follow_ups']; final codeExecRaw = historyMsg != null ? historyMsg['codeExecutions'] ?? historyMsg['code_executions'] : msgData['codeExecutions'] ?? msgData['code_executions']; final rawUsage = _coerceJsonMap(historyMsg?['usage'] ?? msgData['usage']); final errorData = _extractErrorData(msgData, historyMsg); return { 'id': (msgData['id'] ?? _uuid.v4()).toString(), 'content': contentString, 'timestamp': _parseTimestamp(msgData['timestamp']).toIso8601String(), if (msgData['model'] != null) 'model': msgData['model'].toString(), if (files != null) 'files': files, 'sources': _parseSourcesField(sourcesRaw), 'followUps': _coerceStringList(followUpsRaw), 'codeExecutions': _parseCodeExecutionsField(codeExecRaw), if (rawUsage.isNotEmpty) 'usage': rawUsage, if (errorData != null) 'error': errorData, }; } /// Extract error data from OpenWebUI message format. /// OpenWebUI stores errors in a separate 'error' field with 'content' inside. /// Returns a map suitable for ChatMessageError.fromJson(). Map? _extractErrorData( Map msgData, Map? historyMsg, ) { // Check msgData first, then historyMsg final errorRaw = msgData['error'] ?? historyMsg?['error']; if (errorRaw == null) return null; // Handle different error formats from OpenWebUI if (errorRaw is Map) { // Most common: { error: { content: "error message" } } final content = errorRaw['content']; if (content is String && content.isNotEmpty) { return {'content': content}; } // Alternative: { error: { message: "error message" } } final message = errorRaw['message']; if (message is String && message.isNotEmpty) { return {'content': message}; } // Nested error: { error: { error: { message: "..." } } } final nestedError = errorRaw['error']; if (nestedError is Map) { final nestedMessage = nestedError['message']; if (nestedMessage is String && nestedMessage.isNotEmpty) { return {'content': nestedMessage}; } } // FastAPI detail format: { detail: "..." } final detail = errorRaw['detail']; if (detail is String && detail.isNotEmpty) { return {'content': detail}; } // If it's a map but we couldn't extract content, still return an error // to indicate there was an error (matches legacy error === true behavior) return const {'content': null}; } else if (errorRaw is String && errorRaw.isNotEmpty) { // Simple string error return {'content': errorRaw}; } else if (errorRaw == true) { // Legacy format: error === true means content IS the error message // Return a marker so the UI knows this is an error message return const {'content': null}; } return null; } Map _parseOpenWebUIMessageToJson( Map msgData, { Map? historyMsg, }) { dynamic content = msgData['content']; if ((content == null || (content is String && content.isEmpty)) && historyMsg != null && historyMsg['content'] != null) { content = historyMsg['content']; } var contentString = ''; if (content is List) { final buffer = StringBuffer(); for (final entry in content) { if (entry is Map && entry['type'] == 'text') { final text = entry['text']?.toString(); if (text != null && text.isNotEmpty) { buffer.write(text); } } } contentString = buffer.toString(); if (contentString.trim().isEmpty) { final synthesized = _synthesizeToolDetailsFromContentArray(content); if (synthesized.isNotEmpty) { contentString = synthesized; } } } else { contentString = content?.toString() ?? ''; } if (historyMsg != null) { final histContent = historyMsg['content']; if (histContent is String && histContent.length > contentString.length) { contentString = histContent; } else if (histContent is List) { final buf = StringBuffer(); for (final entry in histContent) { if (entry is Map && entry['type'] == 'text') { final text = entry['text']?.toString(); if (text != null && text.isNotEmpty) { buf.write(text); } } } final combined = buf.toString(); if (combined.length > contentString.length) { contentString = combined; } } } final toolCallsList = _extractToolCalls(msgData, historyMsg); if (contentString.trim().isEmpty && toolCallsList != null) { final synthesized = _synthesizeToolDetailsFromToolCalls(toolCallsList); if (synthesized.isNotEmpty) { contentString = synthesized; } } // Extract error field from OpenWebUI - preserve it separately for round-trip final errorData = _extractErrorData(msgData, historyMsg); final role = _resolveRole(msgData); final effectiveFiles = msgData['files'] ?? historyMsg?['files']; List? attachmentIds; List>? files; if (effectiveFiles is List) { final attachments = []; final allFiles = >[]; for (final entry in effectiveFiles) { if (entry is! Map) continue; if (entry['file_id'] != null) { attachments.add(entry['file_id'].toString()); } else if (entry['type'] != null && entry['url'] != null) { final fileMap = { 'type': entry['type'], 'url': entry['url'], }; if (entry['name'] != null) fileMap['name'] = entry['name']; if (entry['size'] != null) fileMap['size'] = entry['size']; final headers = _coerceStringMap(entry['headers']); if (headers != null && headers.isNotEmpty) { fileMap['headers'] = headers; } allFiles.add(fileMap); final url = entry['url'].toString(); // Handle both URL formats: /api/v1/files/{id} and /api/v1/files/{id}/content final match = RegExp( r'/api/v1/files/([^/]+)(?:/content)?$', ).firstMatch(url); if (match != null) { attachments.add(match.group(1)!); } } } attachmentIds = attachments.isNotEmpty ? attachments : null; files = allFiles.isNotEmpty ? allFiles : null; } final statusHistoryRaw = historyMsg != null ? historyMsg['statusHistory'] ?? historyMsg['status_history'] : msgData['statusHistory'] ?? msgData['status_history']; final followUpsRaw = historyMsg != null ? historyMsg['followUps'] ?? historyMsg['follow_ups'] : msgData['followUps'] ?? msgData['follow_ups']; final codeExecRaw = historyMsg != null ? historyMsg['codeExecutions'] ?? historyMsg['code_executions'] : msgData['codeExecutions'] ?? msgData['code_executions']; final sourcesRaw = historyMsg != null ? historyMsg['sources'] ?? historyMsg['citations'] : msgData['sources'] ?? msgData['citations']; // Parse usage data - Open WebUI stores this in 'usage' field on messages final rawUsage = _coerceJsonMap(historyMsg?['usage'] ?? msgData['usage']); final Map? usage = rawUsage.isEmpty ? null : rawUsage; return { 'id': (msgData['id'] ?? _uuid.v4()).toString(), 'role': role, 'content': contentString, 'timestamp': _parseTimestamp(msgData['timestamp']).toIso8601String(), 'model': msgData['model']?.toString(), 'isStreaming': _safeBool(msgData['isStreaming']) ?? false, if (attachmentIds != null) 'attachmentIds': attachmentIds, if (files != null) 'files': files, 'metadata': _coerceJsonMap(msgData['metadata']), 'statusHistory': _parseStatusHistoryField(statusHistoryRaw), 'followUps': _coerceStringList(followUpsRaw), 'codeExecutions': _parseCodeExecutionsField(codeExecRaw), 'sources': _parseSourcesField(sourcesRaw), 'usage': usage, 'versions': const >[], if (errorData != null) 'error': errorData, }; } String _resolveRole(Map msgData) { if (msgData['role'] != null) { return msgData['role'].toString(); } if (msgData['model'] != null) { return 'assistant'; } return 'user'; } /// Build the message chain from history, following parent links from currentId. /// Also collects sibling messages (alternative versions) for each message. List> _buildMessagesListFromHistory( Map history, ) { final messagesMap = history['messages']; final currentId = history['currentId']?.toString(); if (messagesMap is! Map || currentId == null) { return const []; } // Build the main chain from currentId back to root List> buildChain(String? id) { if (id == null) return const []; final raw = messagesMap[id]; if (raw is! Map) return const []; final msg = _coerceJsonMap(raw); msg['id'] = id; final parentId = msg['parentId']?.toString(); if (parentId != null && parentId.isNotEmpty) { return [...buildChain(parentId), msg]; } return [msg]; } final chain = buildChain(currentId); // For each message in the chain, find sibling versions // Siblings are other children of the same parent for (final msg in chain) { final parentId = msg['parentId']?.toString(); if (parentId == null || parentId.isEmpty) continue; final parent = messagesMap[parentId]; if (parent is! Map) continue; final childrenIds = parent['childrenIds']; if (childrenIds is! List || childrenIds.length <= 1) continue; // Collect sibling messages (same role, different id) final msgId = msg['id']?.toString(); final msgRole = msg['role']?.toString(); final siblings = >[]; for (final siblingId in childrenIds) { final sibId = siblingId?.toString(); if (sibId == null || sibId == msgId) continue; final siblingRaw = messagesMap[sibId]; if (siblingRaw is! Map) continue; final sibling = _coerceJsonMap(siblingRaw); final siblingRole = sibling['role']?.toString(); // Only include siblings with the same role (e.g., alternative assistant responses) if (siblingRole == msgRole) { sibling['id'] = sibId; siblings.add(sibling); } } if (siblings.isNotEmpty) { msg['_siblings'] = siblings; } } return chain; } DateTime _parseTimestamp(dynamic timestamp) { if (timestamp == null) return DateTime.now(); if (timestamp is int) { final ts = timestamp > 1000000000000 ? timestamp : timestamp * 1000; return DateTime.fromMillisecondsSinceEpoch(ts); } if (timestamp is String) { final parsedInt = int.tryParse(timestamp); if (parsedInt != null) { final ts = parsedInt > 1000000000000 ? parsedInt : parsedInt * 1000; return DateTime.fromMillisecondsSinceEpoch(ts); } return DateTime.tryParse(timestamp) ?? DateTime.now(); } if (timestamp is double) { final ts = timestamp > 1000000000000 ? timestamp.round() : (timestamp * 1000).round(); return DateTime.fromMillisecondsSinceEpoch(ts); } return DateTime.now(); } List> _parseStatusHistoryField(dynamic raw) { if (raw is List) { final results = >[]; for (final entry in raw) { if (entry is! Map) continue; try { results.add(_coerceJsonMap(entry)); } catch (_) { // Skip malformed status entries to prevent error boundary } } return results; } return const >[]; } Map? _coerceStringMap(dynamic raw) { if (raw is Map) { final result = {}; raw.forEach((key, value) { final keyString = key?.toString(); final valueString = value?.toString(); if (keyString != null && keyString.isNotEmpty && valueString != null && valueString.isNotEmpty) { result[keyString] = valueString; } }); return result.isEmpty ? null : result; } return null; } List _coerceStringList(dynamic raw) { if (raw is List) { return raw .whereType() .map((value) => value?.toString().trim() ?? '') .where((value) => value.isNotEmpty) .toList(growable: false); } if (raw is String && raw.trim().isNotEmpty) { return [raw.trim()]; } return const []; } List> _parseCodeExecutionsField(dynamic raw) { if (raw is List) { return raw .whereType() .map((entry) => _coerceJsonMap(entry)) .toList(growable: false); } return const >[]; } List> _parseSourcesField(dynamic raw) { final normalized = _coerceSourcesList(raw); if (normalized == null || normalized.isEmpty) { return const >[]; } final parsed = parseOpenWebUISourceList(normalized); if (parsed.isNotEmpty) { return parsed .map((reference) => reference.toJson()) .toList(growable: false); } return normalized .whereType() .map(_coerceJsonMap) .toList(growable: false); } List? _coerceSourcesList(dynamic raw) { if (raw is List) { return raw; } if (raw is Iterable) { return raw.toList(growable: false); } if (raw is Map) { return [raw]; } if (raw is String && raw.isNotEmpty) { try { final decoded = jsonDecode(raw); if (decoded is List) { return decoded; } if (decoded is Map) { return [decoded]; } } catch (_) {} } return null; } Map _coerceJsonMap(Object? value) { if (value is Map) { return value.map((key, v) => MapEntry(key.toString(), _coerceJsonValue(v))); } if (value is Map) { final result = {}; value.forEach((key, v) { result[key.toString()] = _coerceJsonValue(v); }); return result; } return {}; } dynamic _coerceJsonValue(dynamic value) { if (value is Map) { return _coerceJsonMap(value); } if (value is List) { return value.map(_coerceJsonValue).toList(); } return value; } String _stringOr(dynamic value, String fallback) { if (value is String && value.isNotEmpty) { return value; } return fallback; } /// Safely parse a boolean from various formats (bool, String, int). bool? _safeBool(dynamic value) { if (value == null) return null; if (value is bool) return value; if (value is String) { final lower = value.toLowerCase(); if (lower == 'true' || lower == '1') return true; if (lower == 'false' || lower == '0') return false; return null; } if (value is num) return value != 0; return null; } String _synthesizeToolDetailsFromToolCalls(List calls) { final buffer = StringBuffer(); for (final rawCall in calls) { final call = Map.from(rawCall); final function = call['function']; final name = (function is Map ? function['name'] : call['name'])?.toString() ?? 'tool'; final id = (call['id']?.toString() ?? 'call_${DateTime.now().millisecondsSinceEpoch}'); final done = call['done']?.toString() ?? 'true'; final argsRaw = function is Map ? function['arguments'] : call['arguments']; final resRaw = call['result'] ?? call['output'] ?? (function is Map ? function['result'] : null); final attrs = StringBuffer() ..write('type="tool_calls"') ..write(' done="${_escapeHtmlAttr(done)}"') ..write(' id="${_escapeHtmlAttr(id)}"') ..write(' name="${_escapeHtmlAttr(name)}"') ..write(' arguments="${_escapeHtmlAttr(_jsonStringify(argsRaw))}"'); final resultStr = _jsonStringify(resRaw); if (resultStr.isNotEmpty) { attrs.write(' result="${_escapeHtmlAttr(resultStr)}"'); } buffer.writeln( '
Tool Executed
', ); } return buffer.toString().trim(); } String _synthesizeToolDetailsFromToolCallsWithResults( List calls, List results, ) { final buffer = StringBuffer(); final resultsMap = >{}; for (final rawResult in results) { final result = Map.from(rawResult); final id = result['tool_call_id']?.toString(); if (id != null) { resultsMap[id] = result; } } for (final rawCall in calls) { final call = Map.from(rawCall); final function = call['function']; final name = (function is Map ? function['name'] : call['name'])?.toString() ?? 'tool'; final id = (call['id']?.toString() ?? 'call_${DateTime.now().millisecondsSinceEpoch}'); final argsRaw = function is Map ? function['arguments'] : call['arguments']; final resultEntry = resultsMap[id]; final resRaw = resultEntry != null ? resultEntry['content'] : null; final filesRaw = resultEntry != null ? resultEntry['files'] : null; final attrs = StringBuffer() ..write('type="tool_calls"') ..write( ' done="${_escapeHtmlAttr(resultEntry != null ? 'true' : 'false')}"', ) ..write(' id="${_escapeHtmlAttr(id)}"') ..write(' name="${_escapeHtmlAttr(name)}"') ..write(' arguments="${_escapeHtmlAttr(_jsonStringify(argsRaw))}"'); final resultStr = _jsonStringify(resRaw); if (resultStr.isNotEmpty) { attrs.write(' result="${_escapeHtmlAttr(resultStr)}"'); } final filesStr = _jsonStringify(filesRaw); if (filesStr.isNotEmpty) { attrs.write(' files="${_escapeHtmlAttr(filesStr)}"'); } buffer.writeln( '
${resultEntry != null ? 'Tool Executed' : 'Executing...'}
', ); } return buffer.toString().trim(); } String _synthesizeToolDetailsFromContentArray(List content) { final buffer = StringBuffer(); for (final item in content) { if (item is! Map) continue; final type = item['type']?.toString(); if (type == null) continue; if (type == 'tool_calls') { final calls = >[]; if (item['content'] is List) { for (final entry in item['content'] as List) { if (entry is Map) { calls.add(Map.from(entry)); } } } final results = >[]; if (item['results'] is List) { for (final entry in item['results'] as List) { if (entry is Map) { results.add(Map.from(entry)); } } } final synthesized = _synthesizeToolDetailsFromToolCallsWithResults( calls, results, ); if (synthesized.isNotEmpty) { buffer.writeln(synthesized); } continue; } if (type == 'tool_call' || type == 'function_call') { final name = (item['name'] ?? item['tool'] ?? 'tool').toString(); final id = (item['id']?.toString() ?? 'call_${DateTime.now().millisecondsSinceEpoch}'); final argsStr = _jsonStringify(item['arguments'] ?? item['args']); final resStr = item['result'] ?? item['output'] ?? item['response']; final attrs = StringBuffer() ..write('type="tool_calls"') ..write(' done="${_escapeHtmlAttr(resStr != null ? 'true' : 'false')}"') ..write(' id="${_escapeHtmlAttr(id)}"') ..write(' name="${_escapeHtmlAttr(name)}"') ..write(' arguments="${_escapeHtmlAttr(argsStr)}"'); final result = _jsonStringify(resStr); if (result.isNotEmpty) { attrs.write(' result="${_escapeHtmlAttr(result)}"'); } buffer.writeln( '
${resStr != null ? 'Tool Executed' : 'Executing...'}
', ); } } return buffer.toString().trim(); } String _jsonStringify(dynamic value) { if (value == null) return ''; try { return jsonEncode(value); } catch (_) { return value.toString(); } } String _escapeHtmlAttr(String value) { return value .replaceAll('&', '&') .replaceAll('"', '"') .replaceAll("'", ''') .replaceAll('<', '<') .replaceAll('>', '>'); } List> parseConversationSummariesWorker( Map payload, ) { final pinnedRaw = payload['pinned']; final archivedRaw = payload['archived']; final regularRaw = payload['regular']; final pinned = >[]; if (pinnedRaw is List) { for (final entry in pinnedRaw) { if (entry is Map) { pinned.add(Map.from(entry)); } } } final archived = >[]; if (archivedRaw is List) { for (final entry in archivedRaw) { if (entry is Map) { archived.add(Map.from(entry)); } } } final regular = >[]; if (regularRaw is List) { for (final entry in regularRaw) { if (entry is Map) { regular.add(Map.from(entry)); } } } final summaries = >[]; final pinnedIds = {}; final archivedIds = {}; for (final entry in pinned) { final summary = parseConversationSummary(entry); summary['pinned'] = true; summaries.add(summary); pinnedIds.add(summary['id'] as String); } for (final entry in archived) { final summary = parseConversationSummary(entry); summary['archived'] = true; summaries.add(summary); archivedIds.add(summary['id'] as String); } for (final entry in regular) { final summary = parseConversationSummary(entry); final id = summary['id'] as String; if (pinnedIds.contains(id) || archivedIds.contains(id)) { continue; } summaries.add(summary); } return summaries; } Map parseFullConversationWorker(Map payload) { final raw = payload['conversation']; if (raw is Map) { return parseFullConversation(raw); } if (raw is Map) { return parseFullConversation(Map.from(raw)); } return parseFullConversation({}); } /// Worker function for parsing folder conversation summaries in a background /// isolate. Takes a list of raw chat data and returns parsed summaries. List> parseFolderSummariesWorker( Map payload, ) { final chatsRaw = payload['chats']; if (chatsRaw is! List) { return const []; } final summaries = >[]; for (final entry in chatsRaw) { if (entry is Map) { final map = entry is Map ? entry : Map.from(entry); summaries.add(parseConversationSummary(map)); } } return summaries; }