| 1 | import '../../../core/models/enums.dart'; |
| 2 | import '../../../core/storage/objectbox_store.dart'; |
| 3 | import '../../../objectbox.g.dart'; |
| 4 | import 'models/message_search_hit.dart'; |
| 5 | |
| 6 | class MessageSearchService { |
| 7 | const MessageSearchService(); |
| 8 | |
| 9 | List<MessageSearchHit> searchMessages( |
| 10 | ObjectBoxStore db, { |
| 11 | required String query, |
| 12 | int limit = 50, |
| 13 | }) { |
| 14 | final normalizedQuery = query.trim().toLowerCase(); |
| 15 | if (normalizedQuery.isEmpty) return const []; |
| 16 | |
| 17 | final hits = <MessageSearchHit>[]; |
| 18 | final conversationIds = <String>{}; |
| 19 | final messageQuery = db.messageBox |
| 20 | .query( |
| 21 | MessageEntity_.content.contains( |
| 22 | normalizedQuery, |
| 23 | caseSensitive: false, |
| 24 | ), |
| 25 | ) |
| 26 | .build(); |
| 27 | final messages = messageQuery.find(); |
| 28 | messageQuery.close(); |
| 29 | |
| 30 | for (final entity in messages) { |
| 31 | if (!entity.content.toLowerCase().contains(normalizedQuery)) continue; |
| 32 | |
| 33 | final content = entity.content; |
| 34 | final index = content.toLowerCase().indexOf(normalizedQuery); |
| 35 | final start = index > 40 ? index - 40 : 0; |
| 36 | final end = (index + normalizedQuery.length + 40).clamp( |
| 37 | 0, |
| 38 | content.length, |
| 39 | ); |
| 40 | var snippet = content.substring(start, end); |
| 41 | if (start > 0) snippet = '…$snippet'; |
| 42 | if (end < content.length) snippet = '$snippet…'; |
| 43 | |
| 44 | conversationIds.add(entity.conversationUid); |
| 45 | hits.add( |
| 46 | MessageSearchHit( |
| 47 | messageId: entity.id, |
| 48 | conversationId: entity.conversationUid, |
| 49 | conversationTitle: 'Chat', |
| 50 | snippet: snippet, |
| 51 | role: MessageRole.values[entity.roleIndex], |
| 52 | ), |
| 53 | ); |
| 54 | if (hits.length >= limit) break; |
| 55 | } |
| 56 | |
| 57 | if (hits.isEmpty) return hits; |
| 58 | |
| 59 | final titleById = _loadConversationTitles(db, conversationIds); |
| 60 | return hits |
| 61 | .map( |
| 62 | (hit) => MessageSearchHit( |
| 63 | messageId: hit.messageId, |
| 64 | conversationId: hit.conversationId, |
| 65 | conversationTitle: titleById[hit.conversationId] ?? 'Chat', |
| 66 | snippet: hit.snippet, |
| 67 | role: hit.role, |
| 68 | ), |
| 69 | ) |
| 70 | .toList(); |
| 71 | } |
| 72 | |
| 73 | Map<String, String> _loadConversationTitles( |
| 74 | ObjectBoxStore db, |
| 75 | Set<String> conversationIds, |
| 76 | ) { |
| 77 | if (conversationIds.isEmpty) return const {}; |
| 78 | |
| 79 | final query = db.conversationBox |
| 80 | .query( |
| 81 | ConversationEntity_.id.oneOf(conversationIds.toList(growable: false)), |
| 82 | ) |
| 83 | .build(); |
| 84 | final entities = query.find(); |
| 85 | query.close(); |
| 86 | |
| 87 | return {for (final entity in entities) entity.id: entity.title}; |
| 88 | } |
| 89 | } |