import '../../../../api/marianumcloud/search/search_chat_messages_response.dart'; import '../../../../api/marianumcloud/talk/chat/get_chat_response.dart'; import '../../../../api/marianumcloud/talk/chat/rich_object_string_processor.dart'; import '../../../../api/marianumcloud/talk/room/get_room_response.dart'; /// One message matching a chat search, either from the loaded history or /// from the server-side search. class ChatSearchHit { final int messageId; final String? actorId; final String? actorDisplayName; final String text; final DateTime? sentAt; final bool isSystem; const ChatSearchHit({ required this.messageId, required this.text, this.actorId, this.actorDisplayName, this.sentAt, this.isSystem = false, }); factory ChatSearchHit.fromServer(SearchChatMessagesEntry entry) => ChatSearchHit( messageId: entry.messageId, text: entry.excerpt, actorId: entry.actorId, actorDisplayName: entry.actorDisplayName, sentAt: entry.sentAt, ); } class ChatMessageMatcher { /// System messages that are folded into other bubbles (reactions, poll /// votes, deletions) and therefore never rendered nor searched as their own /// entry. static bool isHiddenSystemMessage(GetChatResponseObject element) => element.systemMessage.contains('reaction') || element.systemMessage.contains('poll_voted') || element.systemMessage.contains('message_deleted'); /// Newest first. static List findMatches( GetChatResponse response, String query, ) { final q = query.trim().toLowerCase(); if (q.isEmpty) return const []; final matches = []; for (final element in response.data) { if (element.id <= 0 || isHiddenSystemMessage(element)) continue; final text = RichObjectStringProcessor.parseToString( element.message, element.messageParameters, ); final isSystem = element.messageType == GetRoomResponseObjectMessageType.system; final matched = text.toLowerCase().contains(q) || (!isSystem && element.actorDisplayName.toLowerCase().contains(q)); if (!matched) continue; matches.add( ChatSearchHit( messageId: element.id, text: text, actorId: isSystem ? null : element.actorId, actorDisplayName: isSystem ? null : element.actorDisplayName, sentAt: element.sentAt, isSystem: isSystem, ), ); } matches.sort((a, b) => b.sentAt!.compareTo(a.sentAt!)); return matches; } /// Local hits win over server hits for the same message — they carry the /// full text and exact metadata. Result is newest first. static List merge( List local, List server, ) { if (server.isEmpty) return local; final seen = {for (final h in local) h.messageId}; return [...local, ...server.where((h) => seen.add(h.messageId))] ..sort((a, b) => b.messageId.compareTo(a.messageId)); } }